diff --git a/.coveragerc b/.coveragerc index a63e1ae33a5..b468e3ef746 100644 --- a/.coveragerc +++ b/.coveragerc @@ -16,11 +16,16 @@ omit = homeassistant/components/adguard/switch.py homeassistant/components/ads/* homeassistant/components/aftership/sensor.py + homeassistant/components/agent_dvr/__init__.py + homeassistant/components/agent_dvr/camera.py + homeassistant/components/agent_dvr/const.py + homeassistant/components/agent_dvr/helpers.py homeassistant/components/airly/__init__.py homeassistant/components/airly/air_quality.py homeassistant/components/airly/sensor.py homeassistant/components/airly/const.py homeassistant/components/airvisual/__init__.py + homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py homeassistant/components/alarmdecoder/* @@ -57,7 +62,7 @@ omit = homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora_abb_powerone/sensor.py - homeassistant/components/automatic/device_tracker.py + homeassistant/components/automatic/* homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/avri/sensor.py @@ -89,12 +94,16 @@ omit = homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py homeassistant/components/broadlink/const.py + homeassistant/components/broadlink/device.py homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/cover.py + homeassistant/components/bsblan/__init__.py + homeassistant/components/bsblan/climate.py + homeassistant/components/bsblan/const.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py @@ -142,6 +151,9 @@ omit = homeassistant/components/denon/media_player.py homeassistant/components/denonavr/media_player.py homeassistant/components/deutsche_bahn/sensor.py + homeassistant/components/devolo_home_control/__init__.py + homeassistant/components/devolo_home_control/const.py + homeassistant/components/devolo_home_control/switch.py homeassistant/components/dht/sensor.py homeassistant/components/digital_ocean/* homeassistant/components/digitalloggers/switch.py @@ -225,6 +237,9 @@ omit = homeassistant/components/fleetgo/device_tracker.py homeassistant/components/flexit/climate.py homeassistant/components/flic/binary_sensor.py + homeassistant/components/flick_electric/__init__.py + homeassistant/components/flick_electric/const.py + homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py homeassistant/components/flume/* homeassistant/components/flunearyou/__init__.py @@ -298,6 +313,7 @@ omit = homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* + homeassistant/components/home_connect/* homeassistant/components/homematic/* homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py @@ -310,7 +326,11 @@ omit = homeassistant/components/huawei_lte/* homeassistant/components/huawei_router/device_tracker.py homeassistant/components/hue/light.py + homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/scene.py + homeassistant/components/hunterdouglas_powerview/sensor.py + homeassistant/components/hunterdouglas_powerview/cover.py + homeassistant/components/hunterdouglas_powerview/entity.py homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py @@ -343,11 +363,27 @@ omit = homeassistant/components/iqvia/* homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/iss/binary_sensor.py - homeassistant/components/isy994/* + homeassistant/components/isy994/__init__.py + homeassistant/components/isy994/binary_sensor.py + homeassistant/components/isy994/climate.py + homeassistant/components/isy994/cover.py + homeassistant/components/isy994/entity.py + homeassistant/components/isy994/fan.py + homeassistant/components/isy994/helpers.py + homeassistant/components/isy994/light.py + homeassistant/components/isy994/lock.py + homeassistant/components/isy994/sensor.py + homeassistant/components/isy994/services.py + homeassistant/components/isy994/switch.py homeassistant/components/itach/remote.py homeassistant/components/itunes/media_player.py homeassistant/components/joaoapps_join/* - homeassistant/components/juicenet/* + homeassistant/components/juicenet/__init__.py + homeassistant/components/juicenet/const.py + homeassistant/components/juicenet/device.py + homeassistant/components/juicenet/entity.py + homeassistant/components/juicenet/sensor.py + homeassistant/components/juicenet/switch.py homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* @@ -428,6 +464,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/device_tracker.py + homeassistant/components/mill/__init__.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/minecraft_server/__init__.py @@ -502,7 +539,14 @@ omit = homeassistant/components/ombi/* homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py + homeassistant/components/onvif/__init__.py + homeassistant/components/onvif/base.py + homeassistant/components/onvif/binary_sensor.py homeassistant/components/onvif/camera.py + homeassistant/components/onvif/device.py + homeassistant/components/onvif/event.py + homeassistant/components/onvif/parsers.py + homeassistant/components/onvif/sensor.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py @@ -527,7 +571,6 @@ omit = homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/panasonic_bluray/media_player.py - homeassistant/components/panasonic_viera/__init__.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py homeassistant/components/pcal9535a/* @@ -569,7 +612,6 @@ omit = homeassistant/components/qrcode/image_processing.py homeassistant/components/quantum_gateway/device_tracker.py homeassistant/components/qvr_pro/* - homeassistant/components/qwikswitch/* homeassistant/components/rachio/* homeassistant/components/radarr/sensor.py homeassistant/components/radiotherm/climate.py @@ -599,7 +641,6 @@ omit = homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py homeassistant/components/rocketchat/notify.py - homeassistant/components/roku/remote.py homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/braava.py homeassistant/components/roomba/irobot_base.py @@ -608,7 +649,7 @@ omit = homeassistant/components/roomba/vacuum.py homeassistant/components/route53/* homeassistant/components/rova/sensor.py - homeassistant/components/rpi_camera/camera.py + homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* homeassistant/components/rpi_gpio/cover.py homeassistant/components/rpi_gpio_pwm/light.py @@ -672,7 +713,6 @@ omit = homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py - homeassistant/components/songpal/* homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* @@ -773,6 +813,10 @@ omit = homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/unifiled/* + homeassistant/components/upb/__init__.py + homeassistant/components/upb/const.py + homeassistant/components/upb/light.py + homeassistant/components/upb/scene.py homeassistant/components/upcloud/* homeassistant/components/upnp/* homeassistant/components/upc_connect/* @@ -816,6 +860,7 @@ omit = homeassistant/components/webostv/* homeassistant/components/wemo/* homeassistant/components/whois/sensor.py + homeassistant/components/wiffi/* homeassistant/components/wink/* homeassistant/components/wirelesstag/* homeassistant/components/worldtidesinfo/sensor.py @@ -829,7 +874,17 @@ omit = homeassistant/components/xfinity/device_tracker.py homeassistant/components/xiaomi/camera.py homeassistant/components/xiaomi_aqara/* - homeassistant/components/xiaomi_miio/* + homeassistant/components/xiaomi_miio/__init__.py + homeassistant/components/xiaomi_miio/air_quality.py + homeassistant/components/xiaomi_miio/alarm_control_panel.py + homeassistant/components/xiaomi_miio/device_tracker.py + homeassistant/components/xiaomi_miio/fan.py + homeassistant/components/xiaomi_miio/gateway.py + homeassistant/components/xiaomi_miio/light.py + homeassistant/components/xiaomi_miio/remote.py + homeassistant/components/xiaomi_miio/sensor.py + homeassistant/components/xiaomi_miio/switch.py + homeassistant/components/xiaomi_miio/vacuum.py homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* @@ -844,8 +899,9 @@ omit = homeassistant/components/zamg/weather.py homeassistant/components/zengge/light.py homeassistant/components/zeroconf/* + homeassistant/components/zerproc/__init__.py + homeassistant/components/zerproc/const.py homeassistant/components/zestimate/sensor.py - homeassistant/components/zha/__init__.py homeassistant/components/zha/api.py homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/const.py @@ -864,6 +920,10 @@ omit = homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py + homeassistant/components/ozw/__init__.py + homeassistant/components/ozw/discovery.py + homeassistant/components/ozw/entity.py + homeassistant/components/ozw/services.py [report] # Regexes for lines to exclude from consideration diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 00000000000..06de09b5460 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,5 @@ +ignored: + - DL3006 + - DL3008 + - DL3013 + - DL3018 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e491637ea65..491cfc05d8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: diff --git a/.travis.yml b/.travis.yml index 6add8c15bfc..a01398651da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ addons: - libswscale-dev - libswresample-dev - libavfilter-dev + sources: + - sourceline: ppa:savoury1/ffmpeg4 + matrix: fast_finish: true include: diff --git a/CODEOWNERS b/CODEOWNERS index 5f1bb3a7773..fd224174c90 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -15,6 +15,7 @@ homeassistant/scripts/check_config.py @kellerza # Integrations homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck +homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarmdecoder/* @ajschmidt8 @@ -47,12 +48,13 @@ homeassistant/components/avea/* @pattyland homeassistant/components/avri/* @timvancann homeassistant/components/awair/* @danielsjf homeassistant/components/aws/* @awarecan @robbiet480 -homeassistant/components/axis/* @kane610 +homeassistant/components/axis/* @Kane610 homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria +homeassistant/components/blebox/* @gadgetmobile homeassistant/components/blink/* @fronzbot homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @@ -61,6 +63,7 @@ homeassistant/components/braviatv/* @robbiet480 @bieniu homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg +homeassistant/components/bsblan/* @liudger homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties homeassistant/components/cast/* @emontnemery @@ -82,12 +85,13 @@ homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cups/* @fabaff homeassistant/components/daikin/* @fredrike homeassistant/components/darksky/* @fabaff -homeassistant/components/deconz/* @kane610 +homeassistant/components/deconz/* @Kane610 homeassistant/components/delijn/* @bollewolle homeassistant/components/demo/* @home-assistant/core homeassistant/components/denonavr/* @scarface-4711 @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core +homeassistant/components/devolo_home_control/* @2Fake @Shutgun homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek @@ -122,9 +126,11 @@ homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fixer/* @fabaff +homeassistant/components/flick_electric/* @ZephireNZ homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flunearyou/* @bachya +homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio @@ -163,6 +169,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/history/* @home-assistant/core homeassistant/components/hive/* @Rendili @KJonline +homeassistant/components/home_connect/* @DavidMStraub homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco homeassistant/components/homekit_controller/* @Jc2k @@ -174,6 +181,7 @@ homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob +homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame @@ -195,7 +203,7 @@ homeassistant/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 -homeassistant/components/isy994/* @bdraco +homeassistant/components/isy994/* @bdraco @shbatm homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz @@ -239,7 +247,7 @@ homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc @janiversen -homeassistant/components/monoprice/* @etsinko +homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core @emontnemery @@ -267,6 +275,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pvizeli +homeassistant/components/numato/* @clssn homeassistant/components/nut/* @bdraco homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla @@ -275,13 +284,16 @@ homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/onewire/* @garbled1 +homeassistant/components/onvif/* @hunterjm homeassistant/components/openerz/* @misialq +homeassistant/components/opengarage/* @danielhiversen homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff homeassistant/components/opnsense/* @mtreinish homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/oru/* @bvlaicu +homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare homeassistant/components/panasonic_viera/* @joogps homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend @@ -289,7 +301,7 @@ homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi4ioe5v9xxxx/* @antonverburg -homeassistant/components/pi_hole/* @fabaff @johnluetke +homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel @@ -357,13 +369,14 @@ homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/sonarr/* @ctalkington -homeassistant/components/songpal/* @rytilahti +homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen homeassistant/components/spotify/* @frenck homeassistant/components/sql/* @dgomes +homeassistant/components/squeezebox/* @rajlaud homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm @@ -406,12 +419,14 @@ homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli +homeassistant/components/tuya/* @ollo69 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/ubee/* @mzdrale -homeassistant/components/unifi/* @kane610 +homeassistant/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk +homeassistant/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core @@ -436,6 +451,7 @@ homeassistant/components/weather/* @fabaff homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo +homeassistant/components/wiffi/* @mampfes homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck homeassistant/components/workday/* @fabaff @@ -455,6 +471,7 @@ homeassistant/components/yessssms/* @flowolf homeassistant/components/yi/* @bachya homeassistant/components/yr/* @danielhiversen homeassistant/components/zeroconf/* @robbiet480 @Kane610 +homeassistant/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom diff --git a/Dockerfile.dev b/Dockerfile.dev index fa90a84fc1e..be8e2223390 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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 diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 5322360f1c3..da60db941da 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -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" diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 728ee3c5985..2e946b53e5e 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -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() diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 12e27c01504..e6300085299 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -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, diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index b3acaaa6352..65f738b3412 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -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)) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 618a168be61..d53d86f528c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -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) diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 40040d90d0d..c508d0f0240 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -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 diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 916ed2e2613..7175fbc550a 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -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 diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 6e38c11cfcc..d88c2fdd404 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -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 diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index f15c10fc410..b756c79d9de 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -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): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 33431433ef9..2a52663c0e7 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -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): diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json index f6e7039a908..14d570e76e2 100644 --- a/homeassistant/components/abode/strings.json +++ b/homeassistant/components/abode/strings.json @@ -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." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index bbd90442cd9..0985ce5ce2a 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -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): diff --git a/homeassistant/components/abode/translations/en.json b/homeassistant/components/abode/translations/en.json index feaef16fdff..ae33c6bed04 100644 --- a/homeassistant/components/abode/translations/en.json +++ b/homeassistant/components/abode/translations/en.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Password", - "username": "Email Address" + "username": "Email" }, "title": "Fill in your Abode login information" } diff --git a/homeassistant/components/abode/translations/fi.json b/homeassistant/components/abode/translations/fi.json new file mode 100644 index 00000000000..f236327a22c --- /dev/null +++ b/homeassistant/components/abode/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_error": "Yhteytt\u00e4 Abodeen ei voi muodostaa." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/ko.json b/homeassistant/components/abode/translations/ko.json index 46363382407..a72d461252a 100644 --- a/homeassistant/components/abode/translations/ko.json +++ b/homeassistant/components/abode/translations/ko.json @@ -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" } } } diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json index dc269b112d7..60c1ad897f1 100644 --- a/homeassistant/components/abode/translations/no.json +++ b/homeassistant/components/abode/translations/no.json @@ -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": { diff --git a/homeassistant/components/abode/translations/pl.json b/homeassistant/components/abode/translations/pl.json index d7a25bb20b7..6efd1ae885c 100644 --- a/homeassistant/components/abode/translations/pl.json +++ b/homeassistant/components/abode/translations/pl.json @@ -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" } diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json index 5120d529cb5..7a97cbda3c5 100644 --- a/homeassistant/components/abode/translations/zh-Hant.json +++ b/homeassistant/components/abode/translations/zh-Hant.json @@ -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" } diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 85ff4a3f5b1..861e483adb8 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -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": [] } diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 9afc9963522..f947f3fe0c0 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -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): diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 6996a2b0d51..f968f524f3d 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -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", } diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index 471708879b4..f5f780c70b5 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -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" } diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 1ddefb3367b..78d2769ce5d 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -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__( diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 90c3ddcb359..cc68f3e3b28 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -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" diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index adabb83ab0b..06b355273fe 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -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.", diff --git a/homeassistant/components/adguard/translations/da.json b/homeassistant/components/adguard/translations/da.json index 3a1a73ac6bd..9d3460b1ed2 100644 --- a/homeassistant/components/adguard/translations/da.json +++ b/homeassistant/components/adguard/translations/da.json @@ -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" diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json index 5a36b35d028..c0ce604fbee 100644 --- a/homeassistant/components/adguard/translations/es-419.json +++ b/homeassistant/components/adguard/translations/es-419.json @@ -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" diff --git a/homeassistant/components/adguard/translations/fi.json b/homeassistant/components/adguard/translations/fi.json new file mode 100644 index 00000000000..4765338a5e3 --- /dev/null +++ b/homeassistant/components/adguard/translations/fi.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "connection_error": "Yhdist\u00e4minen ep\u00e4onnistui." + }, + "step": { + "user": { + "data": { + "host": "Palvelin", + "port": "Portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/he.json b/homeassistant/components/adguard/translations/he.json new file mode 100644 index 00000000000..49c18fac88c --- /dev/null +++ b/homeassistant/components/adguard/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/id.json b/homeassistant/components/adguard/translations/id.json index 3548361e396..c8357cad0f6 100644 --- a/homeassistant/components/adguard/translations/id.json +++ b/homeassistant/components/adguard/translations/id.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "password": "Kata sandi", - "port": "Port" + "password": "Kata sandi" } } } diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index 1bcc60c80f0..b5b77e434ca 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -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" } } } diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json index 4c839fc9e18..76c85138976 100644 --- a/homeassistant/components/adguard/translations/lb.json +++ b/homeassistant/components/adguard/translations/lb.json @@ -16,7 +16,7 @@ }, "user": { "data": { - "host": "Apparat", + "host": "Host", "password": "Passwuert", "port": "Port", "ssl": "AdGuard Home benotzt een SSL Zertifikat", diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 4e3439dd624..427894eeff4 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -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" diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index 5194e799b16..60385c586e2 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -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.", diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index 71264e906e4..6cf2de9163d 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -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.", diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json index 605085af1f1..e2dcdf7d312 100644 --- a/homeassistant/components/adguard/translations/pt-BR.json +++ b/homeassistant/components/adguard/translations/pt-BR.json @@ -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" diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index 77ce7025f70..b4642359973 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "host": "Servidor", "password": "Palavra-passe", - "port": "Porta", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 8c83b8c024c..1287b408544 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -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" } } diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index 8ae4a5481d2..2a7e4d7a40d 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -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" diff --git a/homeassistant/components/adguard/translations/vi.json b/homeassistant/components/adguard/translations/vi.json index 1b76fef5671..1d2ea273f93 100644 --- a/homeassistant/components/adguard/translations/vi.json +++ b/homeassistant/components/adguard/translations/vi.json @@ -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" } } diff --git a/homeassistant/components/adguard/translations/zh-Hans.json b/homeassistant/components/adguard/translations/zh-Hans.json index 7c52a9d1ac0..4204beb5268 100644 --- a/homeassistant/components/adguard/translations/zh-Hans.json +++ b/homeassistant/components/adguard/translations/zh-Hans.json @@ -7,7 +7,6 @@ "user": { "data": { "password": "\u5bc6\u7801", - "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" } } diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 9e2f7b0cc4a..df8a74dc1d5 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -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): diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index 0fdcbc16ef8..1a350b3e39f 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -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__( diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index 384bd2e83a6..74701066078 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -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): diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 64c797ff309..c8231d8a31d 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -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): diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py new file mode 100644 index 00000000000..e11e61a4126 --- /dev/null +++ b/homeassistant/components/agent_dvr/__init__.py @@ -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 diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py new file mode 100644 index 00000000000..ebc0eda222f --- /dev/null +++ b/homeassistant/components/agent_dvr/camera.py @@ -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() diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py new file mode 100644 index 00000000000..a5c98ade1cb --- /dev/null +++ b/homeassistant/components/agent_dvr/config_flow.py @@ -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) diff --git a/homeassistant/components/agent_dvr/const.py b/homeassistant/components/agent_dvr/const.py new file mode 100644 index 00000000000..e571edf9800 --- /dev/null +++ b/homeassistant/components/agent_dvr/const.py @@ -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" diff --git a/homeassistant/components/agent_dvr/helpers.py b/homeassistant/components/agent_dvr/helpers.py new file mode 100644 index 00000000000..028a683946d --- /dev/null +++ b/homeassistant/components/agent_dvr/helpers.py @@ -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}/" diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json new file mode 100644 index 00000000000..1244326d494 --- /dev/null +++ b/homeassistant/components/agent_dvr/manifest.json @@ -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"] +} diff --git a/homeassistant/components/agent_dvr/services.yaml b/homeassistant/components/agent_dvr/services.yaml new file mode 100644 index 00000000000..8bf1e01269a --- /dev/null +++ b/homeassistant/components/agent_dvr/services.yaml @@ -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" diff --git a/homeassistant/components/agent_dvr/strings.json b/homeassistant/components/agent_dvr/strings.json new file mode 100644 index 00000000000..95f99231083 --- /dev/null +++ b/homeassistant/components/agent_dvr/strings.json @@ -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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ca.json b/homeassistant/components/agent_dvr/translations/ca.json new file mode 100644 index 00000000000..401f2166bd1 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/ca.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json new file mode 100644 index 00000000000..7f5588fcb6f --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/de.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/en.json b/homeassistant/components/agent_dvr/translations/en.json new file mode 100644 index 00000000000..0f110a06863 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/en.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/es.json b/homeassistant/components/agent_dvr/translations/es.json new file mode 100644 index 00000000000..7d071f621ff --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/es.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/fi.json b/homeassistant/components/agent_dvr/translations/fi.json new file mode 100644 index 00000000000..e824a41aad2 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/fi.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/fr.json b/homeassistant/components/agent_dvr/translations/fr.json new file mode 100644 index 00000000000..dc9a372a0c0 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/fr.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/he.json b/homeassistant/components/agent_dvr/translations/he.json new file mode 100644 index 00000000000..3ab81e908ec --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/he.json @@ -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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/it.json b/homeassistant/components/agent_dvr/translations/it.json new file mode 100644 index 00000000000..1db719893aa --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/it.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ko.json b/homeassistant/components/agent_dvr/translations/ko.json new file mode 100644 index 00000000000..dc72c932d69 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/ko.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/lb.json b/homeassistant/components/agent_dvr/translations/lb.json new file mode 100644 index 00000000000..8b56a09a17e --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/lb.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/no.json b/homeassistant/components/agent_dvr/translations/no.json new file mode 100644 index 00000000000..cbb4e3503a0 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/no.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/pl.json b/homeassistant/components/agent_dvr/translations/pl.json new file mode 100644 index 00000000000..6f36cf2d7d4 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/pl.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/ru.json b/homeassistant/components/agent_dvr/translations/ru.json new file mode 100644 index 00000000000..69b192fc710 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/ru.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/sv.json b/homeassistant/components/agent_dvr/translations/sv.json new file mode 100644 index 00000000000..537f6c0e11b --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/sv.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant/components/agent_dvr/translations/zh-Hant.json new file mode 100644 index 00000000000..dba3e0937b7 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/zh-Hant.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index ec7b8cb3c76..a0c5975188b 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -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.""" diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 794f70901f3..8bf7782e910 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -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." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 76e8702e8fc..3caf870ccdf 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -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" } } diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index ab243699232..2dd164823cc 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -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" diff --git a/homeassistant/components/airly/translations/ko.json b/homeassistant/components/airly/translations/ko.json index 5283797eb79..3faebcd5744 100644 --- a/homeassistant/components/airly/translations/ko.json +++ b/homeassistant/components/airly/translations/ko.json @@ -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" diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index 965b12ef1fe..5d4f6d31785 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -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": "" } } diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 3cc43883308..1b4b56c7656 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -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" diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index d594922cd8f..fc6c58cf978 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -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" diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 4352b15b8a5..4c46e7b3e7d 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -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 diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py new file mode 100644 index 00000000000..bd1c10a9d84 --- /dev/null +++ b/homeassistant/components/airvisual/air_quality.py @@ -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() + }, + } + ) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 0c9c0e65ff1..abbc2df9061 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -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.""" diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py index ab54e191116..a98a899b762 100644 --- a/homeassistant/components/airvisual/const.py +++ b/homeassistant/components/airvisual/const.py @@ -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" diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index d5c7dc6853d..93b57a4804e 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -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"] } diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 20e76bf86b6..b122f3c27b4 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -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" + ) diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index cd81d1862dd..c7e2bc34701 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -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" + } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index 2d9a644c704..e285e68e186 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -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" }, diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index a8b2d296560..5e66daa3919 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -14,7 +14,8 @@ "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" - } + }, + "title": "Konfigurieren Sie eine Geografie" }, "node_pro": { "data": { diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index a3af2a7031b..6298193f59b 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -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 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index ada76676ca9..77c7ed8dd71 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -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" diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index e64b2f31691..0bd06eaf7bd 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -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" } } diff --git a/homeassistant/components/airvisual/translations/fi.json b/homeassistant/components/airvisual/translations/fi.json new file mode 100644 index 00000000000..b193b59de26 --- /dev/null +++ b/homeassistant/components/airvisual/translations/fi.json @@ -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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index d2013b0f17d..73efe479b2c 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -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" } diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json new file mode 100644 index 00000000000..7d1a1c696ed --- /dev/null +++ b/homeassistant/components/airvisual/translations/he.json @@ -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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/hi.json b/homeassistant/components/airvisual/translations/hi.json index ff9c8ebe206..42fbf50a829 100644 --- a/homeassistant/components/airvisual/translations/hi.json +++ b/homeassistant/components/airvisual/translations/hi.json @@ -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" }, diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json new file mode 100644 index 00000000000..c8a33625953 --- /dev/null +++ b/homeassistant/components/airvisual/translations/hu.json @@ -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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 268c82bfccf..9831011c7b2 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -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" } } diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index c28790288ab..e60e0a7ef1c 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -1,20 +1,42 @@ { "config": { "abort": { - "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc88c\ud45c\uac12 \ub610\ub294 Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" + "general_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "unable_to_connect": "AirVisual Node/Pro \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { - "user": { + "geography": { "data": { "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4" }, - "description": "\uc9c0\ub9ac\uc801 \uc704\uce58\uc5d0\uc11c \ub300\uae30\uc9c8\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", - "title": "AirVisual \uad6c\uc131" + "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc9c0\ub9ac\uc801 \uc704\uce58\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" + }, + "node_pro": { + "data": { + "ip_address": "\uae30\uae30 IP \uc8fc\uc18c/\ud638\uc2a4\ud2b8 \uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30" + }, + "user": { + "data": { + "api_key": "API \ud0a4", + "cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "node_pro": "AirVisual Node Pro", + "type": "\uc5f0\ub3d9 \uc720\ud615" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 AirVisual \ub370\uc774\ud130 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "AirVisual \uad6c\uc131\ud558\uae30" } } }, @@ -25,7 +47,7 @@ "show_on_map": "\uc9c0\ub3c4\uc5d0 \ubaa8\ub2c8\ud130\ub9c1\ub41c \uc9c0\ub9ac \uc815\ubcf4 \ud45c\uc2dc" }, "description": "AirVisual \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ub2e4\uc591\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "AirVisual \uad6c\uc131" + "title": "AirVisual \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json new file mode 100644 index 00000000000..102b08cb91e --- /dev/null +++ b/homeassistant/components/airvisual/translations/nl.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd." + }, + "error": { + "general_error": "Er is een onbekende fout opgetreden.", + "unable_to_connect": "Kan geen verbinding maken met Node / Pro-apparaat." + }, + "step": { + "geography": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.", + "title": "Configureer een geografie" + }, + "node_pro": { + "data": { + "ip_address": "IP adres/hostname van component", + "password": "Wachtwoord van component" + }, + "description": "Monitor een persoonlijke AirVisual-eenheid. Het wachtwoord kan worden opgehaald uit de gebruikersinterface van het apparaat.", + "title": "Configureer een AirVisual Node / Pro" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "cloud_api": "Geografische ligging", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "node_pro": "AirVisual Node Pro", + "type": "Integratietype" + }, + "description": "Kies welk type AirVisual-gegevens u wilt bewaken.", + "title": "Configureer AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Toon gecontroleerde geografie op de kaart" + }, + "description": "Stel verschillende opties in voor de AirVisual-integratie.", + "title": "Configureer AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index a528c98e04c..888e1f58e35 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -5,18 +5,18 @@ }, "error": { "general_error": "Det oppstod en ukjent feil.", - "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "invalid_api_key": "Ugyldig API-n\u00f8kkel angitt.", "unable_to_connect": "Kan ikke koble til Node / Pro-enheten." }, "step": { "geography": { "data": { - "api_key": "API-n\u00f8kkel", + "api_key": "API n\u00f8kkel", "latitude": "Breddegrad", "longitude": "Lengdegrad" }, "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en geografisk plassering.", - "title": "Konfigurer en geografi" + "title": "Konfigurer en Geography" }, "node_pro": { "data": { @@ -32,7 +32,7 @@ "cloud_api": "Geografisk plassering", "latitude": "Breddegrad", "longitude": "Lengdegrad", - "node_pro": "AirVisual Node Pro", + "node_pro": "", "type": "Integrasjonstype" }, "description": "Velg hvilken type AirVisual-data du vil overv\u00e5ke.", diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 8687a2ead03..7873a18bc7e 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -4,30 +4,31 @@ "already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu." }, "error": { - "general_error": "Nieznany b\u0142\u0105d", - "invalid_api_key": "Nieprawid\u0142owy klucz API", + "general_error": "[%key_id:common::config_flow::error::unknown%]", + "invalid_api_key": "Nieprawid\u0142owy klucz API.", "unable_to_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z jednostk\u0105 Node/Pro." }, "step": { "geography": { "data": { - "api_key": "Klucz API", + "api_key": "[%key_id:common::config_flow::data::api_key%]", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, + "description": "U\u017cyj interfejsu API chmury AirVisual do monitorowania lokalizacji geograficznej.", "title": "Konfiguracja Geography" }, "node_pro": { "data": { - "ip_address": "Nazwa hosta lub adres IP jednostki", - "password": "Has\u0142o jednostki" + "ip_address": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%] jednostki" }, - "description": "Has\u0142o", + "description": "Monitoruj jednostk\u0119 AirVisual. Has\u0142o mo\u017cna odzyska\u0107 z interfejsu u\u017cytkownika urz\u0105dzenia.", "title": "Konfiguracja AirVisual Node/Pro" }, "user": { "data": { - "api_key": "Klucz API", + "api_key": "[%key_id:common::config_flow::data::api_key%]", "cloud_api": "Lokalizacja geograficzna", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", diff --git a/homeassistant/components/airvisual/translations/sl.json b/homeassistant/components/airvisual/translations/sl.json index 376da5c3900..18765467fd9 100644 --- a/homeassistant/components/airvisual/translations/sl.json +++ b/homeassistant/components/airvisual/translations/sl.json @@ -4,14 +4,36 @@ "already_configured": "Te koordinate so \u017ee registrirane." }, "error": { - "invalid_api_key": "Neveljaven API klju\u010d" + "general_error": "Pri\u0161lo je do neznane napake.", + "invalid_api_key": "Vpisan neveljaven API klju\u010d", + "unable_to_connect": "Ni mogo\u010de povezati z enoto Node/Pro." }, "step": { + "geography": { + "data": { + "api_key": "Klju\u010d API", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina" + }, + "description": "Uporabite API oblaka AirVisual za spremljanje geografske lokacije.", + "title": "Konfigurirajte lokacijo" + }, + "node_pro": { + "data": { + "ip_address": "IP naslov/ime gostitelja enote", + "password": "Geslo enote" + }, + "description": "Spremljajte osebno napravo AirVisual. Geslo je mogo\u010de pridobiti iz uporabni\u0161kega vmesnika enote.", + "title": "Konfigurirajte AirVisual Node/Pro" + }, "user": { "data": { "api_key": "API Klju\u010d", + "cloud_api": "Geografska lokacija", "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina" + "longitude": "Zemljepisna dol\u017eina", + "node_pro": "AirVisual Node Pro", + "type": "Vrsta integracije" }, "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", "title": "Nastavite AirVisual" diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json new file mode 100644 index 00000000000..4c54fcf002c --- /dev/null +++ b/homeassistant/components/airvisual/translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade." + }, + "step": { + "geography": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, + "node_pro": { + "data": { + "ip_address": "Enhets IP-adress / v\u00e4rdnamn", + "password": "Enhetsl\u00f6senord" + } + }, + "user": { + "data": { + "api_key": "API-nyckel", + "cloud_api": "Geografisk Plats", + "latitude": "Latitud", + "longitude": "Longitud", + "type": "Integrationstyp" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 1153d7c3b99..36dd078d7b5 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -21,7 +21,7 @@ "node_pro": { "data": { "ip_address": "\u8a2d\u5099 IP \u4f4d\u5740/\u4e3b\u6a5f\u540d\u7a31", - "password": "\u8a2d\u5099\u5bc6\u78bc" + "password": "\u5bc6\u78bc" }, "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u8a2d\u5099\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u8a2d\u5099 UI \u7372\u5f97\u3002", "title": "\u8a2d\u5b9a AirVisual Node/Pro" diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index eaa2dfc85f0..8b61d29b78a 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -8,7 +8,7 @@ from homeassistant.components.cover import ( PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, - CoverDevice, + CoverEntity, ) from homeassistant.const import ( CONF_PASSWORD, @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class AladdinDevice(CoverDevice): +class AladdinDevice(CoverEntity): """Representation of Aladdin Connect cover.""" def __init__(self, acc, device): diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 67b0309e513..50b8adb4c03 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -32,6 +32,8 @@ from .const import ( SUPPORT_ALARM_TRIGGER, ) +_LOGGER = logging.getLogger(__name__) + DOMAIN = "alarm_control_panel" SCAN_INTERVAL = timedelta(seconds=30) ATTR_CHANGED_BY = "changed_by" @@ -99,8 +101,8 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class AlarmControlPanel(Entity): - """An abstract class for alarm control devices.""" +class AlarmControlPanelEntity(Entity): + """An abstract class for alarm control entities.""" @property def code_format(self): @@ -179,3 +181,15 @@ class AlarmControlPanel(Entity): ATTR_CODE_ARM_REQUIRED: self.code_arm_required, } return state_attr + + +class AlarmControlPanel(AlarmControlPanelEntity): + """An abstract class for alarm control entities (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "AlarmControlPanel is deprecated, modify %s to extend AlarmControlPanelEntity", + cls.__name__, + ) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 95ae17aaaf5..849da062665 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -19,6 +19,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -32,6 +33,7 @@ from . import DOMAIN TRIGGER_TYPES = { "triggered", "disarmed", + "arming", "armed_home", "armed_away", "armed_night", @@ -79,6 +81,13 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_ENTITY_ID: entry.entity_id, CONF_TYPE: "triggered", }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arming", + }, ] if supported_features & SUPPORT_ALARM_ARM_HOME: triggers.append( @@ -122,29 +131,32 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" config = TRIGGER_SCHEMA(config) + from_state = None if config[CONF_TYPE] == "triggered": - from_state = STATE_ALARM_PENDING to_state = STATE_ALARM_TRIGGERED elif config[CONF_TYPE] == "disarmed": - from_state = STATE_ALARM_TRIGGERED to_state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == "arming": + from_state = STATE_ALARM_DISARMED + to_state = STATE_ALARM_ARMING elif config[CONF_TYPE] == "armed_home": - from_state = STATE_ALARM_PENDING + from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_HOME elif config[CONF_TYPE] == "armed_away": - from_state = STATE_ALARM_PENDING + from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_AWAY elif config[CONF_TYPE] == "armed_night": - from_state = STATE_ALARM_PENDING + from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_NIGHT state_config = { state.CONF_PLATFORM: "state", CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, state.CONF_TO: to_state, } + if from_state: + state_config[state.CONF_FROM] = from_state state_config = state.TRIGGER_SCHEMA(state_config) return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index 465dd0e8994..fc76102d0fe 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -8,16 +8,16 @@ "trigger": "Lanzar {entity_name}" }, "condition_type": { - "is_armed_away": "{entity_name} est\u00e1 armada fuera", + "is_armed_away": "{entity_name} est\u00e1 armada ausente", "is_armed_home": "{entity_name} est\u00e1 armada en casa", "is_armed_night": "{entity_name} est\u00e1 armada noche", "is_disarmed": "{entity_name} est\u00e1 desarmada", "is_triggered": "{entity_name} est\u00e1 disparada" }, "trigger_type": { - "armed_away": "{entity_name} armado fuera", - "armed_home": "{entity_name} armado en casa", - "armed_night": "{entity_name} armado modo noche", + "armed_away": "{entity_name} armada ausente", + "armed_home": "{entity_name} armada en casa", + "armed_night": "{entity_name} armada noche", "disarmed": "{entity_name} desarmado", "triggered": "{entity_name} activado" } @@ -25,10 +25,10 @@ "state": { "_": { "armed": "Armado", - "armed_away": "Armado fuera de casa", - "armed_custom_bypass": "Armada Zona Espec\u00edfica", - "armed_home": "Armado en casa", - "armed_night": "Armado noche", + "armed_away": "Armada ausente", + "armed_custom_bypass": "Armada personalizada", + "armed_home": "Armada en casa", + "armed_night": "Armada noche", "arming": "Armando", "disarmed": "Desarmado", "disarming": "Desarmando", diff --git a/homeassistant/components/alarm_control_panel/translations/it.json b/homeassistant/components/alarm_control_panel/translations/it.json index a365c5cd35b..1574f88541b 100644 --- a/homeassistant/components/alarm_control_panel/translations/it.json +++ b/homeassistant/components/alarm_control_panel/translations/it.json @@ -26,12 +26,12 @@ "_": { "armed": "Attivo", "armed_away": "Attivo fuori casa", - "armed_custom_bypass": "Attivo con bypass", + "armed_custom_bypass": "Attivo con bypass personalizzato", "armed_home": "Attivo in casa", "armed_night": "Attivo Notte", - "arming": "In attivazione", + "arming": "In Attivazione", "disarmed": "Disattivo", - "disarming": "In disattivazione", + "disarming": "In Disattivazione", "pending": "In sospeso", "triggered": "Attivato" } diff --git a/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant/components/alarm_control_panel/translations/nl.json index a1a00e7c9e3..15b5fd8457c 100644 --- a/homeassistant/components/alarm_control_panel/translations/nl.json +++ b/homeassistant/components/alarm_control_panel/translations/nl.json @@ -7,6 +7,13 @@ "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} afwezig ingeschakeld", + "is_armed_home": "{entity_name} thuis ingeschakeld", + "is_armed_night": "{entity_name} nachtstand ingeschakeld", + "is_disarmed": "{entity_name} is uitgeschakeld", + "is_triggered": "{entity_name} wordt geactiveerd" + }, "trigger_type": { "armed_away": "{entity_name} afwezig ingeschakeld", "armed_home": "{entity_name} thuis ingeschakeld", @@ -18,7 +25,7 @@ "state": { "_": { "armed": "Ingeschakeld", - "armed_away": "Ingeschakeld afwezig", + "armed_away": "Afwezig Ingeschakeld", "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", "armed_home": "Ingeschakeld thuis", "armed_night": "Ingeschakeld nacht", diff --git a/homeassistant/components/alarm_control_panel/translations/no.json b/homeassistant/components/alarm_control_panel/translations/no.json index d26d7d0b181..465dd250086 100644 --- a/homeassistant/components/alarm_control_panel/translations/no.json +++ b/homeassistant/components/alarm_control_panel/translations/no.json @@ -8,9 +8,9 @@ "trigger": "Utl\u00f8ser {entity_name}" }, "condition_type": { - "is_armed_away": "{entity_name} aktivert borte", - "is_armed_home": "{entity_name} aktivert hjemme", - "is_armed_night": "{entity_name} aktivert natt", + "is_armed_away": "{entity_name} er aktivert borte", + "is_armed_home": "{entity_name} er aktivert hjemme", + "is_armed_night": "{entity_name} er aktivert natt", "is_disarmed": "{entity_name} er deaktivert", "is_triggered": "{entity_name} er utl\u00f8st" }, diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 5625204c762..ac90ea1796f 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -74,7 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class AlarmDecoderAlarmPanel(AlarmControlPanel): +class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Representation of an AlarmDecoder-based alarm panel.""" def __init__(self, auto_bypass, code_arm_required): diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index b34c90bc35a..cec1b8356b0 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -1,7 +1,7 @@ """Support for AlarmDecoder zone states- represented as binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import ( CONF_RELAY_ADDR, @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class AlarmDecoderBinarySensor(BinarySensorDevice): +class AlarmDecoderBinarySensor(BinarySensorEntity): """Representation of an AlarmDecoder binary sensor.""" def __init__( diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py new file mode 100644 index 00000000000..7645b642d59 --- /dev/null +++ b/homeassistant/components/alert/reproduce_state.py @@ -0,0 +1,76 @@ +"""Reproduce an Alert state.""" +import asyncio +import logging +from typing import Any, Dict, Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, + state: State, + *, + context: Optional[Context] = None, + reproduce_options: Optional[Dict[str, Any]] = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, + states: Iterable[State], + *, + context: Optional[Context] = None, + reproduce_options: Optional[Dict[str, Any]] = None, +) -> None: + """Reproduce Alert states.""" + # Reproduce states in parallel. + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e22c5c62db9..e74722b89e8 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,7 +1,6 @@ """Alexa entity adapters.""" import logging from typing import List -from urllib.parse import urlparse from homeassistant.components import ( alarm_control_panel, @@ -386,7 +385,7 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class == cover.DEVICE_CLASS_GARAGE: + if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): return [DisplayCategory.GARAGE_DOOR] if device_class == cover.DEVICE_CLASS_DOOR: return [DisplayCategory.DOOR] @@ -408,7 +407,7 @@ class CoverCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class != cover.DEVICE_CLASS_GARAGE: + if device_class not in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -799,8 +798,15 @@ class CameraCapabilities(AlexaEntity): ) return False - url = urlparse(network.async_get_external_url(self.hass)) - if url.scheme != "https": + try: + network.get_url( + self.hass, + allow_internal=False, + allow_ip=False, + require_ssl=True, + require_standard_port=True, + ) + except network.NoURLAvailableError: _LOGGER.debug( "%s requires HTTPS for AlexaCameraStreamController", self.entity_id ) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 6b903665c17..7737016d573 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1533,7 +1533,18 @@ async def async_api_initialize_camera_stream(hass, config, directive, context): entity = directive.entity stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls") camera_image = hass.states.get(entity.entity_id).attributes["entity_picture"] - external_url = network.async_get_external_url(hass) + + try: + external_url = network.get_url( + hass, + allow_internal=False, + allow_ip=False, + require_ssl=True, + require_standard_port=True, + ) + except network.NoURLAvailableError: + raise AlexaInvalidValueError("Failed to find suitable URL to serve to Alexa") + payload = { "cameraStreams": [ { diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 58fada7a196..07daa3e4781 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -147,16 +147,15 @@ async def _configure_almond_for_ha( hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI ): """Configure Almond to connect to HA.""" - - if entry.data["type"] == TYPE_OAUTH2: - # If we're connecting over OAuth2, we will only set up connection - # with Home Assistant if we're remotely accessible. - hass_url = network.async_get_external_url(hass) - else: - hass_url = hass.config.api.base_url - - # If hass_url is None, we're not going to configure Almond to connect to HA. - if hass_url is None: + try: + if entry.data["type"] == TYPE_OAUTH2: + # If we're connecting over OAuth2, we will only set up connection + # with Home Assistant if we're remotely accessible. + hass_url = network.get_url(hass, allow_internal=False, prefer_cloud=True) + else: + hass_url = network.get_url(hass) + except network.NoURLAvailableError: + # If no URL is available, we're not going to configure Almond to connect to HA. return _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) diff --git a/homeassistant/components/almond/translations/es-419.json b/homeassistant/components/almond/translations/es-419.json index 7b7f7aea9ca..fbcf901c2e5 100644 --- a/homeassistant/components/almond/translations/es-419.json +++ b/homeassistant/components/almond/translations/es-419.json @@ -2,7 +2,17 @@ "config": { "abort": { "already_setup": "Solo puede configurar una cuenta Almond.", - "cannot_connect": "No se puede conectar con el servidor Almond." + "cannot_connect": "No se puede conectar con el servidor Almond.", + "missing_configuration": "Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon}?", + "title": "Almond a trav\u00e9s del complemento Hass.io" + }, + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fi.json b/homeassistant/components/almond/translations/fi.json new file mode 100644 index 00000000000..33427bf8451 --- /dev/null +++ b/homeassistant/components/almond/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Yhteyden muodostaminen Almond-palvelimeen ei onnistu." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index 695ca3a752c..645eaafab08 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -11,7 +11,7 @@ "title": "Hass.io \uc560\ub4dc\uc628\uc758 Almond" }, "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index d27b903452d..3a6a89a8340 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -8,10 +8,10 @@ "step": { "hassio_confirm": { "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?", - "title": "Almond via Hass.io add-on" + "title": "" }, "pick_implementation": { - "title": "Velg autentiseringsmetode" + "title": "Velg godkjenningsmetode" } } } diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json index 6b3feb4bd0b..ed60372d5b3 100644 --- a/homeassistant/components/almond/translations/pl.json +++ b/homeassistant/components/almond/translations/pl.json @@ -11,7 +11,7 @@ "title": "Almond poprzez dodatek Hass.io" }, "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelniania" + "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" } } } diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index 3821a65e08b..a4fa0f5d46c 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -11,7 +11,7 @@ "title": "Almond (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" }, "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } } diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index dad5fc88e80..28103980869 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,6 +2,6 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": ["alpha_vantage==2.1.3"], + "requirements": ["alpha_vantage==2.2.0"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index a8ed166903e..cb19d1329ca 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -5,7 +5,7 @@ import logging import ambiclimate import voluptuous as vol -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -130,7 +130,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class AmbiclimateEntity(ClimateDevice): +class AmbiclimateEntity(ClimateEntity): """Representation of a Ambiclimate Thermostat device.""" def __init__(self, heater, store): diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 4996a458a1f..99d4aa3c944 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.network import get_url from .const import ( AUTH_CALLBACK_NAME, @@ -122,16 +123,15 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow): clientsession = async_get_clientsession(self.hass) callback_url = self._cb_url() - oauth = ambiclimate.AmbiclimateOAuth( + return ambiclimate.AmbiclimateOAuth( config.get(CONF_CLIENT_ID), config.get(CONF_CLIENT_SECRET), callback_url, clientsession, ) - return oauth def _cb_url(self): - return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" + return f"{get_url(self.hass)}{AUTH_CALLBACK_PATH}" async def _get_authorize_url(self): oauth = self._generate_oauth() diff --git a/homeassistant/components/ambiclimate/strings.json b/homeassistant/components/ambiclimate/strings.json index 50bc8284b71..26af78f8915 100644 --- a/homeassistant/components/ambiclimate/strings.json +++ b/homeassistant/components/ambiclimate/strings.json @@ -3,7 +3,7 @@ "step": { "auth": { "title": "Authenticate Ambiclimate", - "description": "Please follow this [link]({authorization_url}) and Allow access to your Ambiclimate account, then come back and press Submit below.\n(Make sure the specified callback url is {cb_url})" + "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback url is {cb_url})" } }, "create_entry": { diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json index d260691eb90..0b8ca963813 100644 --- a/homeassistant/components/ambiclimate/translations/ca.json +++ b/homeassistant/components/ambiclimate/translations/ca.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.", - "already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.", + "access_token": "S'ha produ\u00eft un error desconegut al generat un token d'acc\u00e9s.", + "already_setup": "El compte d'Ambi Climate est\u00e0 configurat.", "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { diff --git a/homeassistant/components/ambiclimate/translations/ko.json b/homeassistant/components/ambiclimate/translations/ko.json index 311e05fa19e..2717d4c4b79 100644 --- a/homeassistant/components/ambiclimate/translations/ko.json +++ b/homeassistant/components/ambiclimate/translations/ko.json @@ -15,7 +15,7 @@ "step": { "auth": { "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 \ud5c8\uc6a9 \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", - "title": "Ambi Climate \uc778\uc99d" + "title": "Ambi Climate \uc778\uc99d\ud558\uae30" } } } diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json index e0df96e0361..13529d2e1af 100644 --- a/homeassistant/components/ambiclimate/translations/no.json +++ b/homeassistant/components/ambiclimate/translations/no.json @@ -3,19 +3,19 @@ "abort": { "access_token": "Ukjent feil ved oppretting av tilgangstoken.", "already_setup": "Ambiclimate-kontoen er konfigurert.", - "no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Du m\u00e5 konfigurere Ambiclimate f\u00f8r du kan godkjenne den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { - "default": "Vellykket autentisering med Ambiclimate" + "default": "Vellykket godkjenning med Ambiclimate" }, "error": { "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", - "no_token": "Ikke autentisert med Ambiclimate" + "no_token": "Ikke godkjent med Ambiclimate" }, "step": { "auth": { "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, kom deretter tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", - "title": "Autensiere Ambiclimate" + "title": "Godkjenn Ambiclimate" } } } diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index d1b1f9b8f1d..5aba9d637d4 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Ambient Weather Station binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_NAME from homeassistant.core import callback @@ -54,7 +54,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(binary_sensor_list, True) -class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice): +class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): """Define an Ambient binary sensor.""" @property diff --git a/homeassistant/components/ambient_station/strings.json b/homeassistant/components/ambient_station/strings.json index 0e49301198c..54b3dd05511 100644 --- a/homeassistant/components/ambient_station/strings.json +++ b/homeassistant/components/ambient_station/strings.json @@ -3,13 +3,18 @@ "step": { "user": { "title": "Fill in your information", - "data": { "api_key": "API Key", "app_key": "Application Key" } + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "app_key": "Application Key" + } } }, "error": { "invalid_key": "Invalid API Key and/or Application Key", "no_devices": "No devices found in account" }, - "abort": { "already_configured": "This app key is already in use." } + "abort": { + "already_configured": "This app key is already in use." + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/es-419.json b/homeassistant/components/ambient_station/translations/es-419.json index d2c60aee5a0..b16c5af9c62 100644 --- a/homeassistant/components/ambient_station/translations/es-419.json +++ b/homeassistant/components/ambient_station/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Esta clave de aplicaci\u00f3n ya est\u00e1 en uso." + }, "error": { "invalid_key": "Clave de API y/o clave de aplicaci\u00f3n no v\u00e1lida", "no_devices": "No se han encontrado dispositivos en la cuenta." diff --git a/homeassistant/components/ambient_station/translations/fi.json b/homeassistant/components/ambient_station/translations/fi.json new file mode 100644 index 00000000000..acb097c2d7d --- /dev/null +++ b/homeassistant/components/ambient_station/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-avain", + "app_key": "Sovellusavain" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/ko.json b/homeassistant/components/ambient_station/translations/ko.json index 83d273dc4df..d4e227656c2 100644 --- a/homeassistant/components/ambient_station/translations/ko.json +++ b/homeassistant/components/ambient_station/translations/ko.json @@ -13,7 +13,7 @@ "api_key": "API \ud0a4", "app_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30" } } } diff --git a/homeassistant/components/ambient_station/translations/pl.json b/homeassistant/components/ambient_station/translations/pl.json index bb597971b0c..01a0c83bd28 100644 --- a/homeassistant/components/ambient_station/translations/pl.json +++ b/homeassistant/components/ambient_station/translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Klucz API", + "api_key": "[%key_id:common::config_flow::data::api_key%]", "app_key": "Klucz aplikacji" }, "title": "Wprowad\u017a dane" diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 40cb755bd98..a3057211f2a 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -7,7 +7,7 @@ from amcrest import AmcrestError from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME from homeassistant.core import callback @@ -63,7 +63,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class AmcrestBinarySensor(BinarySensorDevice): +class AmcrestBinarySensor(BinarySensorEntity): """Binary sensor for Amcrest camera.""" def __init__(self, name, device, sensor_type): diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index 0e9cca46afb..14384565718 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -1,5 +1,5 @@ """Support for Android IP Webcam binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity @@ -16,7 +16,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True) -class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): +class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity): """Representation of an IP Webcam binary sensor.""" def __init__(self, name, host, ipcam, sensor): diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index a494e78bdc7..bdbb37e7661 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,5 +1,5 @@ """Support for Android IP Webcam settings.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import ( CONF_HOST, @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(all_switches, True) -class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): +class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): """An abstract class for an IP Webcam setting.""" def __init__(self, name, host, ipcam, setting): diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index a9d7f0ad5be..44085273940 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -16,7 +16,7 @@ from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -373,7 +373,7 @@ def adb_decorator(override_available=False): return _adb_decorator -class ADBDevice(MediaPlayerDevice): +class ADBDevice(MediaPlayerEntity): """Representation of an Android TV or Fire TV device.""" def __init__( diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index cc6011d9b65..c769f51d5b6 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -5,7 +5,7 @@ import logging from anel_pwrctrl import DeviceMaster 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_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class PwrCtrlSwitch(SwitchDevice): +class PwrCtrlSwitch(SwitchEntity): """Representation of a PwrCtrl switch.""" def __init__(self, port, parent_device): diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 434692ce6f5..788fa8db7eb 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -4,7 +4,7 @@ import logging import anthemav import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -79,7 +79,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([device]) -class AnthemAVR(MediaPlayerDevice): +class AnthemAVR(MediaPlayerEntity): """Entity reading values from Anthem AVR protocol.""" def __init__(self, avr, name): diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 000e738052d..daf9592f3e6 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,7 +1,7 @@ """Support for tracking the online status of a UPS.""" import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -20,7 +20,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([OnlineStatus(config, apcups_data)], True) -class OnlineStatus(BinarySensorDevice): +class OnlineStatus(BinarySensorEntity): """Representation of an UPS online status.""" def __init__(self, config, data): diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 8b9f3355930..72e7d88b364 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -3,7 +3,7 @@ import logging import pyatv.const as atv_const -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, @@ -76,7 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([entity]) -class AppleTvDevice(MediaPlayerDevice): +class AppleTvDevice(MediaPlayerEntity): """Representation of an Apple TV device.""" def __init__(self, atv, name, power): diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index dd784cc449d..4f935ba0ab8 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -17,7 +17,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([AppleTVRemote(atv, power, name)]) -class AppleTVRemote(remote.RemoteDevice): +class AppleTVRemote(remote.RemoteEntity): """Device that sends commands to an Apple TV.""" def __init__(self, atv, power, name): diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index d949175bc6e..94822eaef18 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -4,7 +4,7 @@ import logging from aqualogic.core import States 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_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -45,7 +45,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(switches) -class AquaLogicSwitch(SwitchDevice): +class AquaLogicSwitch(SwitchEntity): """Switch implementation for the AquaLogic component.""" def __init__(self, processor, switch_type): diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index d5383590868..35c7e2ae646 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -4,7 +4,7 @@ import logging import sharp_aquos_rc import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -121,7 +121,7 @@ def _retry(func): return wrapper -class SharpAquosTVDevice(MediaPlayerDevice): +class SharpAquosTVDevice(MediaPlayerEntity): """Representation of a Aquos TV.""" def __init__(self, name, remote, power_on_enabled=False): diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 59bcd08a641..aa11e66d49c 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -162,3 +162,8 @@ async def _run_client(hass, client, interval): await asyncio.sleep(interval) except asyncio.TimeoutError: continue + except asyncio.CancelledError: + return + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception, aborting arcam client") + return diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 5508f4b6869..c304d7bf351 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,6 +3,6 @@ "name": "Arcam FMJ Receivers", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.4.3"], + "requirements": ["arcam-fmj==0.4.4"], "codeowners": ["@elupus"] } diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 92e07a0547e..125b3bf96b1 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -6,7 +6,7 @@ from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceC from arcam.fmj.state import State from homeassistant import config_entries -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOUND_MODE, @@ -57,13 +57,14 @@ async def async_setup_entry( zone_config.get(SERVICE_TURN_ON), ) for zone, zone_config in config[CONF_ZONE].items() - ] + ], + True, ) return True -class ArcamFmj(MediaPlayerDevice): +class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" def __init__(self, state: State, name: str, turn_on: Optional[ConfigType]): @@ -86,7 +87,12 @@ class ArcamFmj(MediaPlayerDevice): audio_format, _ = self._state.get_incoming_audio_format() return bool( audio_format - in (IncomingAudioFormat.PCM, IncomingAudioFormat.ANALOGUE_DIRECT, None) + in ( + IncomingAudioFormat.PCM, + IncomingAudioFormat.ANALOGUE_DIRECT, + IncomingAudioFormat.UNDETECTED, + None, + ) ) @property diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json index b78b8cbaa7b..d8a4c453015 100644 --- a/homeassistant/components/arcam_fmj/translations/no.json +++ b/homeassistant/components/arcam_fmj/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Arcam FMJ" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py index ea6f36ac7f5..99b73c86fdb 100644 --- a/homeassistant/components/arduino/switch.py +++ b/homeassistant/components/arduino/switch.py @@ -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 @@ -41,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class ArduinoSwitch(SwitchDevice): +class ArduinoSwitch(SwitchEntity): """Representation of an Arduino switch.""" def __init__(self, pin, options, board): diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 1b914f80aa7..3cd9038f1a8 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -8,7 +8,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, @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ArestBinarySensor(BinarySensorDevice): +class ArestBinarySensor(BinarySensorEntity): """Implement an aREST binary sensor for a pin.""" def __init__(self, arest, resource, name, device_class, pin): diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 638c0e2557a..7e50b1df8ff 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -205,7 +205,7 @@ class ArestData: try: if str(self._pin[0]) == "A": response = requests.get( - f"{self._resource,}/analog/{self._pin[1:]}", timeout=10 + f"{self._resource}/analog/{self._pin[1:]}", timeout=10 ) self.data = {"value": response.json()["return_value"]} except TypeError: diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 875211f5f0b..ddd6b51f76d 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -5,7 +5,7 @@ import logging import requests 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, CONF_RESOURCE, HTTP_OK import homeassistant.helpers.config_validation as cv @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class ArestSwitchBase(SwitchDevice): +class ArestSwitchBase(SwitchEntity): """Representation of an aREST switch.""" def __init__(self, resource, location, name): diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 7440db0495e..47328d5cbc2 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -66,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(base_stations, True) -class ArloBaseStation(AlarmControlPanel): +class ArloBaseStation(AlarmControlPanelEntity): """Representation of an Arlo Alarm Control Panel.""" def __init__(self, data, home_mode_name, away_mode_name, night_mode_name): diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index cb90df1650c..e80ae0cc79e 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -3,24 +3,12 @@ from datetime import timedelta import logging import async_timeout -from pyatag import AtagDataStore, AtagException +from pyatag import AtagException, AtagOne from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.water_heater import DOMAIN as WATER_HEATER from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_ID, - ATTR_MODE, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - PRESSURE_BAR, - TEMP_CELSIUS, -) from homeassistant.core import HomeAssistant, asyncio from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -30,94 +18,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda _LOGGER = logging.getLogger(__name__) DOMAIN = "atag" -DATA_LISTENER = f"{DOMAIN}_listener" -SIGNAL_UPDATE_ATAG = f"{DOMAIN}_update" PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] -HOUR = "h" -FIRE = "fire" -PERCENTAGE = "%" - -ICONS = { - TEMP_CELSIUS: "mdi:thermometer", - PRESSURE_BAR: "mdi:gauge", - FIRE: "mdi:fire", - ATTR_MODE: "mdi:settings", -} - -ENTITY_TYPES = { - SENSOR: [ - { - ATTR_NAME: "Outside Temperature", - ATTR_ID: "outside_temp", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: ICONS[TEMP_CELSIUS], - }, - { - ATTR_NAME: "Average Outside Temperature", - ATTR_ID: "tout_avg", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: ICONS[TEMP_CELSIUS], - }, - { - ATTR_NAME: "Weather Status", - ATTR_ID: "weather_status", - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: None, - }, - { - ATTR_NAME: "CH Water Pressure", - ATTR_ID: "ch_water_pres", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_BAR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: ICONS[PRESSURE_BAR], - }, - { - ATTR_NAME: "CH Water Temperature", - ATTR_ID: "ch_water_temp", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: ICONS[TEMP_CELSIUS], - }, - { - ATTR_NAME: "CH Return Temperature", - ATTR_ID: "ch_return_temp", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: ICONS[TEMP_CELSIUS], - }, - { - ATTR_NAME: "Burning Hours", - ATTR_ID: "burning_hours", - ATTR_UNIT_OF_MEASUREMENT: HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: ICONS[FIRE], - }, - { - ATTR_NAME: "Flame", - ATTR_ID: "rel_mod_level", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: ICONS[FIRE], - }, - ], - CLIMATE: { - ATTR_NAME: DOMAIN.title(), - ATTR_ID: CLIMATE, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: None, - }, - WATER_HEATER: { - ATTR_NAME: DOMAIN.title(), - ATTR_ID: WATER_HEATER, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: None, - }, -} async def async_setup(hass: HomeAssistant, config): @@ -130,8 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): session = async_get_clientsession(hass) coordinator = AtagDataUpdateCoordinator(hass, session, entry) - - await coordinator.async_refresh() + try: + await coordinator.async_refresh() + except AtagException: + raise ConfigEntryNotReady if not coordinator.last_update_success: raise ConfigEntryNotReady @@ -152,7 +55,7 @@ class AtagDataUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass, session, entry): """Initialize.""" - self.atag = AtagDataStore(session, paired=True, **entry.data) + self.atag = AtagOne(session=session, **entry.data) super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) @@ -162,11 +65,12 @@ class AtagDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" with async_timeout.timeout(20): try: - await self.atag.async_update() - except (AtagException) as error: + await self.atag.update() + if not self.atag.report: + raise UpdateFailed("No data") + except AtagException as error: raise UpdateFailed(error) - - return self.atag.sensordata + return self.atag.report async def async_unload_entry(hass, entry): @@ -187,24 +91,21 @@ async def async_unload_entry(hass, entry): class AtagEntity(Entity): """Defines a base Atag entity.""" - def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_type: dict) -> None: + def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None: """Initialize the Atag entity.""" self.coordinator = coordinator - self._id = atag_type[ATTR_ID] - self._name = atag_type[ATTR_NAME] - self._icon = atag_type[ATTR_ICON] - self._unit = atag_type[ATTR_UNIT_OF_MEASUREMENT] - self._class = atag_type[ATTR_DEVICE_CLASS] + self._id = atag_id + self._name = DOMAIN.title() @property def device_info(self) -> dict: """Return info for device registry.""" - device = self.coordinator.atag.device + device = self.coordinator.atag.id version = self.coordinator.atag.apiversion return { "identifiers": {(DOMAIN, device)}, - ATTR_NAME: "Atag Thermostat", + "name": "Atag Thermostat", "model": "Atag One", "sw_version": version, "manufacturer": "Atag", @@ -215,14 +116,6 @@ class AtagEntity(Entity): """Return the name of the entity.""" return self._name - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - self._icon = ( - self.coordinator.data.get(self._id, {}).get(ATTR_ICON) or self._icon - ) - return self._icon - @property def should_poll(self) -> bool: """Return the polling requirement of the entity.""" @@ -231,12 +124,7 @@ class AtagEntity(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return self._unit - - @property - def device_class(self): - """Return the device class.""" - return self._class + return self.coordinator.atag.climate.temp_unit @property def available(self): @@ -246,7 +134,7 @@ class AtagEntity(Entity): @property def unique_id(self): """Return a unique ID to use for this entity.""" - return f"{self.coordinator.atag.device}-{self._id}" + return f"{self.coordinator.atag.id}-{self._id}" async def async_added_to_hass(self): """Connect to dispatcher listening for entity data notifications.""" diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 40bd8cd4cc7..4c39b2ea8f8 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -1,7 +1,7 @@ """Initialization of ATAG One climate platform.""" from typing import List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -14,7 +14,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from . import CLIMATE, DOMAIN, ENTITY_TYPES, AtagEntity +from . import CLIMATE, DOMAIN, AtagEntity PRESET_SCHEDULE = "Auto" PRESET_MANUAL = "Manual" @@ -33,10 +33,10 @@ HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] async def async_setup_entry(hass, entry, async_add_entities): """Load a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([AtagThermostat(coordinator, ENTITY_TYPES[CLIMATE])]) + async_add_entities([AtagThermostat(coordinator, CLIMATE)]) -class AtagThermostat(AtagEntity, ClimateDevice): +class AtagThermostat(AtagEntity, ClimateEntity): """Atag climate device.""" @property @@ -47,8 +47,8 @@ class AtagThermostat(AtagEntity, ClimateDevice): @property def hvac_mode(self) -> Optional[str]: """Return hvac operation ie. heat, cool mode.""" - if self.coordinator.atag.hvac_mode in HVAC_MODES: - return self.coordinator.atag.hvac_mode + if self.coordinator.atag.climate.hvac_mode in HVAC_MODES: + return self.coordinator.atag.climate.hvac_mode return None @property @@ -59,31 +59,31 @@ class AtagThermostat(AtagEntity, ClimateDevice): @property def hvac_action(self) -> Optional[str]: """Return the current running hvac operation.""" - if self.coordinator.atag.cv_status: + if self.coordinator.atag.climate.status: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @property def temperature_unit(self): """Return the unit of measurement.""" - if self.coordinator.atag.temp_unit in [TEMP_CELSIUS, TEMP_FAHRENHEIT]: - return self.coordinator.atag.temp_unit + if self.coordinator.atag.climate.temp_unit in [TEMP_CELSIUS, TEMP_FAHRENHEIT]: + return self.coordinator.atag.climate.temp_unit return None @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - return self.coordinator.atag.temperature + return self.coordinator.atag.climate.temperature @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - return self.coordinator.atag.target_temperature + return self.coordinator.atag.climate.target_temperature @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., auto, manual, fireplace, extend, etc.""" - return self.coordinator.atag.hold_mode + return self.coordinator.atag.climate.preset_mode @property def preset_modes(self) -> Optional[List[str]]: @@ -92,15 +92,15 @@ class AtagThermostat(AtagEntity, ClimateDevice): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - await self.coordinator.atag.set_temp(kwargs.get(ATTR_TEMPERATURE)) + await self.coordinator.atag.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.coordinator.atag.set_hvac_mode(hvac_mode) + await self.coordinator.atag.climate.set_hvac_mode(hvac_mode) self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self.coordinator.atag.set_hold_mode(preset_mode) + await self.coordinator.atag.climate.set_preset_mode(preset_mode) self.async_write_ha_state() diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index 27b2b7a42f6..369f4b98587 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -1,9 +1,9 @@ """Config flow for the Atag component.""" -from pyatag import DEFAULT_PORT, AtagDataStore, AtagException +from pyatag import DEFAULT_PORT, AtagException, AtagOne import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -11,6 +11,7 @@ from . import DOMAIN # pylint: disable=unused-import DATA_SCHEMA = { vol.Required(CONF_HOST): str, + vol.Optional(CONF_EMAIL): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), } @@ -31,14 +32,15 @@ class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._show_form() session = async_get_clientsession(self.hass) try: - atag = AtagDataStore(session, **user_input) - await atag.async_check_pair_status() + atag = AtagOne(session=session, **user_input) + await atag.authorize() + await atag.update(force=True) except AtagException: return await self._show_form({"base": "connection_error"}) - user_input.update({CONF_DEVICE: atag.device}) - return self.async_create_entry(title=atag.device, data=user_input) + user_input.update({CONF_DEVICE: atag.id}) + return self.async_create_entry(title=atag.id, data=user_input) @callback async def _show_form(self, errors=None): diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 902da2bff75..5fd77ee5155 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,6 +3,6 @@ "name": "Atag", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", - "requirements": ["pyatag==0.2.19"], + "requirements": ["pyatag==0.3.1.2"], "codeowners": ["@MatsNL"] } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 743b50ef40d..d5ff0b7bbde 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -1,14 +1,31 @@ """Initialization of ATAG One sensor platform.""" -from homeassistant.const import ATTR_STATE +from homeassistant.const import ( + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_BAR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) -from . import DOMAIN, ENTITY_TYPES, SENSOR, AtagEntity +from . import DOMAIN, AtagEntity + +SENSORS = { + "Outside Temperature": "outside_temp", + "Average Outside Temperature": "tout_avg", + "Weather Status": "weather_status", + "CH Water Pressure": "ch_water_pres", + "CH Water Temperature": "ch_water_temp", + "CH Return Temperature": "ch_return_temp", + "Burning Hours": "burning_hours", + "Flame": "rel_mod_level", +} async def async_setup_entry(hass, config_entry, async_add_entities): """Initialize sensor platform from config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for sensor in ENTITY_TYPES[SENSOR]: + for sensor in SENSORS: entities.append(AtagSensor(coordinator, sensor)) async_add_entities(entities) @@ -16,7 +33,38 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AtagSensor(AtagEntity): """Representation of a AtagOne Sensor.""" + def __init__(self, coordinator, sensor): + """Initialize Atag sensor.""" + super().__init__(coordinator, SENSORS[sensor]) + self._name = sensor + @property def state(self): """Return the state of the sensor.""" - return self.coordinator.data[self._id][ATTR_STATE] + return self.coordinator.data[self._id].state + + @property + def icon(self): + """Return icon.""" + return self.coordinator.data[self._id].icon + + @property + def device_class(self): + """Return deviceclass.""" + if self.coordinator.data[self._id].sensorclass in [ + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ]: + return self.coordinator.data[self._id].sensorclass + return None + + @property + def unit_of_measurement(self): + """Return measure.""" + if self.coordinator.data[self._id].measure in [ + PRESSURE_BAR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + ]: + return self.coordinator.data[self._id].measure + return None diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json index 094fde70dc9..859f0531c5a 100644 --- a/homeassistant/components/atag/strings.json +++ b/homeassistant/components/atag/strings.json @@ -5,8 +5,9 @@ "user": { "title": "Connect to the device", "data": { - "host": "Host", - "port": "Port (10000)" + "host": "[%key:common::config_flow::data::host%]", + "email": "Email (Optional)", + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -17,4 +18,4 @@ "already_configured": "Only one Atag device can be added to Home Assistant" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/ca.json b/homeassistant/components/atag/translations/ca.json index 994cc3c8fbe..4208873e675 100644 --- a/homeassistant/components/atag/translations/ca.json +++ b/homeassistant/components/atag/translations/ca.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "email": "Correu electr\u00f2nic (opcional)", "host": "Amfitri\u00f3", "port": "Port (10000)" }, diff --git a/homeassistant/components/atag/translations/en.json b/homeassistant/components/atag/translations/en.json index edee94a8e04..978f3a15453 100644 --- a/homeassistant/components/atag/translations/en.json +++ b/homeassistant/components/atag/translations/en.json @@ -9,8 +9,9 @@ "step": { "user": { "data": { + "email": "Email (Optional)", "host": "Host", - "port": "Port (10000)" + "port": "Port" }, "title": "Connect to the device" } diff --git a/homeassistant/components/atag/translations/es-419.json b/homeassistant/components/atag/translations/es-419.json new file mode 100644 index 00000000000..214dd0e9004 --- /dev/null +++ b/homeassistant/components/atag/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Solo se puede agregar un dispositivo Atag a Home Assistant" + }, + "error": { + "connection_error": "No se pudo conectar, intente nuevamente" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto (10000)" + }, + "title": "Conectarse al dispositivo" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/fi.json b/homeassistant/components/atag/translations/fi.json new file mode 100644 index 00000000000..60a920588b0 --- /dev/null +++ b/homeassistant/components/atag/translations/fi.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "connection_error": "Yhteyden muodostaminen ep\u00e4onnistui. Yrit\u00e4 uudelleen" + }, + "step": { + "user": { + "data": { + "host": "Palvelin", + "port": "Portti (10000)" + }, + "title": "Yhdist\u00e4 laitteeseen" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/fr.json b/homeassistant/components/atag/translations/fr.json index ace565408f6..1972bd10da4 100644 --- a/homeassistant/components/atag/translations/fr.json +++ b/homeassistant/components/atag/translations/fr.json @@ -1,13 +1,21 @@ { "config": { + "abort": { + "already_configured": "Un seul appareil Atag peut \u00eatre ajout\u00e9 \u00e0 Home Assistant" + }, + "error": { + "connection_error": "Impossible de se connecter, veuillez r\u00e9essayer" + }, "step": { "user": { "data": { + "email": "Courriel (facultatif)", "host": "H\u00f4te", "port": "Port (10000)" }, "title": "Se connecter \u00e0 l'appareil" } } - } + }, + "title": "Atag" } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/he.json b/homeassistant/components/atag/translations/he.json new file mode 100644 index 00000000000..9212fe8f93f --- /dev/null +++ b/homeassistant/components/atag/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Payload (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json new file mode 100644 index 00000000000..22687b6944a --- /dev/null +++ b/homeassistant/components/atag/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port (10000)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/it.json b/homeassistant/components/atag/translations/it.json new file mode 100644 index 00000000000..190da0f14d7 --- /dev/null +++ b/homeassistant/components/atag/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00c8 possibile aggiungere un solo dispositivo Atag ad Home Assistant" + }, + "error": { + "connection_error": "Impossibile connettersi, si prega di riprovare" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta (10000)" + }, + "title": "Connettersi al dispositivo" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/ko.json b/homeassistant/components/atag/translations/ko.json new file mode 100644 index 00000000000..97558185815 --- /dev/null +++ b/homeassistant/components/atag/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Home Assistant \uc5d0\ub294 \ud558\ub098\uc758 Atag \uae30\uae30\ub9cc \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "email": "\uc774\uba54\uc77c (\uc120\ud0dd \uc0ac\ud56d)", + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/lb.json b/homeassistant/components/atag/translations/lb.json index dcb32f3eedc..3850ab33419 100644 --- a/homeassistant/components/atag/translations/lb.json +++ b/homeassistant/components/atag/translations/lb.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "email": "E-Mail (Optionell)", "host": "Apparat", "port": "Port (10000)" }, diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 14da45b8eb9..049e363cf92 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -16,5 +16,5 @@ } } }, - "title": "Atag" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/no.json b/homeassistant/components/atag/translations/no.json index 4b4a5346558..ee9811c581f 100644 --- a/homeassistant/components/atag/translations/no.json +++ b/homeassistant/components/atag/translations/no.json @@ -9,12 +9,13 @@ "step": { "user": { "data": { + "email": "E-post (valgfritt)", "host": "Vert", - "port": "Port (10000)" + "port": "Port " }, "title": "Koble til enheten" } } }, - "title": "Atag " + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/pl.json b/homeassistant/components/atag/translations/pl.json index e931c1fc10f..a1d7c281dc9 100644 --- a/homeassistant/components/atag/translations/pl.json +++ b/homeassistant/components/atag/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Do Home Assistant mo\u017cna doda\u0107 tylko jedno urz\u0105dzenie Atag" + "already_configured": "Do Home Assistant mo\u017cna doda\u0107 tylko jedno urz\u0105dzenie Atag." }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." @@ -9,8 +9,9 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port (10000)" + "email": "[%key_id:common::config_flow::data::email%] (opcjonalnie)", + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%] (10000)" }, "title": "Po\u0142\u0105cz z urz\u0105dzeniem" } diff --git a/homeassistant/components/atag/translations/ru.json b/homeassistant/components/atag/translations/ru.json index f1c734dc933..daaff9d9dda 100644 --- a/homeassistant/components/atag/translations/ru.json +++ b/homeassistant/components/atag/translations/ru.json @@ -9,8 +9,9 @@ "step": { "user": { "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442 (10000)" + "port": "\u041f\u043e\u0440\u0442" }, "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } diff --git a/homeassistant/components/atag/translations/sl.json b/homeassistant/components/atag/translations/sl.json new file mode 100644 index 00000000000..6f9b1e23759 --- /dev/null +++ b/homeassistant/components/atag/translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Home Assistant-u lahko dodate samo eno napravo Atag" + }, + "error": { + "connection_error": "Povezava ni uspela, poskusite znova" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata (10000)" + }, + "title": "Pove\u017eite se z napravo" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/sv.json b/homeassistant/components/atag/translations/sv.json new file mode 100644 index 00000000000..938a0191ee3 --- /dev/null +++ b/homeassistant/components/atag/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_error": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port (10000)" + }, + "title": "Anslut till enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index aa1c6a90d2b..46d39b186ac 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -9,8 +9,9 @@ "step": { "user": { "data": { + "email": "\u90f5\u4ef6\uff08\u9078\u9805\uff09", "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0\uff0810000\uff09" + "port": "\u901a\u8a0a\u57e0" }, "title": "\u9023\u7dda\u81f3\u8a2d\u5099" } diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index bb1f72d6a8e..311ff56985b 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -3,11 +3,11 @@ from homeassistant.components.water_heater import ( ATTR_TEMPERATURE, STATE_ECO, STATE_PERFORMANCE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import STATE_OFF, TEMP_CELSIUS -from . import DOMAIN, ENTITY_TYPES, WATER_HEATER, AtagEntity +from . import DOMAIN, WATER_HEATER, AtagEntity SUPPORT_FLAGS_HEATER = 0 OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] @@ -16,10 +16,10 @@ OPERATION_LIST = [STATE_OFF, STATE_ECO, STATE_PERFORMANCE] async def async_setup_entry(hass, config_entry, async_add_entities): """Initialize DHW device from config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([AtagWaterHeater(coordinator, ENTITY_TYPES[WATER_HEATER])]) + async_add_entities([AtagWaterHeater(coordinator, WATER_HEATER)]) -class AtagWaterHeater(AtagEntity, WaterHeaterDevice): +class AtagWaterHeater(AtagEntity, WaterHeaterEntity): """Representation of an ATAG water heater.""" @property @@ -35,12 +35,12 @@ class AtagWaterHeater(AtagEntity, WaterHeaterDevice): @property def current_temperature(self): """Return the current temperature.""" - return self.coordinator.atag.dhw_temperature + return self.coordinator.atag.dhw.temperature @property def current_operation(self): """Return current operation.""" - if self.coordinator.atag.dhw_status: + if self.coordinator.atag.dhw.status: return STATE_PERFORMANCE return STATE_OFF @@ -49,22 +49,22 @@ class AtagWaterHeater(AtagEntity, WaterHeaterDevice): """List of available operation modes.""" return OPERATION_LIST - async def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): + if await self.coordinator.atag.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): self.async_write_ha_state() @property def target_temperature(self): """Return the setpoint if water demand, otherwise return base temp (comfort level).""" - return self.coordinator.atag.dhw_target_temperature + return self.coordinator.atag.dhw.target_temperature @property def max_temp(self): """Return the maximum temperature.""" - return self.coordinator.atag.dhw_max_temp + return self.coordinator.atag.dhw.max_temp @property def min_temp(self): """Return the minimum temperature.""" - return self.coordinator.atag.dhw_min_temp + return self.coordinator.atag.dhw.min_temp diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 2ec6ec4b83d..e5970fc4d3b 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.switch import ( DEVICE_CLASS_OUTLET, PLATFORM_SCHEMA, - SwitchDevice, + SwitchEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME from homeassistant.exceptions import PlatformNotReady @@ -64,7 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(switches) -class AtenSwitch(SwitchDevice): +class AtenSwitch(SwitchEntity): """Represents an ATEN PE switch.""" def __init__(self, device, mac, outlet, name): diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index e61de730302..6602cfe8661 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -108,7 +108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) -class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorDevice): +class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): """Representation of an August Door binary sensor.""" def __init__(self, data, sensor_type, device): @@ -155,7 +155,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorDevice): return f"{self._device_id}_open" -class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorDevice): +class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Representation of an August binary sensor.""" def __init__(self, data, sensor_type, device): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 495c215edad..e16c603d919 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -5,7 +5,7 @@ from august.activity import ActivityType from august.lock import LockStatus from august.util import update_lock_detail_from_activity -from homeassistant.components.lock import ATTR_CHANGED_BY, LockDevice +from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) -class AugustLock(AugustEntityMixin, RestoreEntity, LockDevice): +class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): """Representation of an August lock.""" def __init__(self, data, device): diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index bffca81ab33..880c13c7fe2 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -5,23 +5,27 @@ "cannot_connect": "Failed to connect, please try again", "invalid_auth": "Invalid authentication" }, - "abort": { "already_configured": "Account is already configured" }, + "abort": { + "already_configured": "Account is already configured" + }, "step": { "validation": { "title": "Two factor authentication", - "data": { "code": "Verification code" }, + "data": { + "code": "Verification code" + }, "description": "Please check your {login_method} ({username}) and enter the verification code below" }, "user": { "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", "data": { "timeout": "Timeout (seconds)", - "password": "Password", - "username": "Username", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", "login_method": "Login Method" }, "title": "Setup an August account" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/es-419.json b/homeassistant/components/august/translations/es-419.json index 0732c1c5e48..914aea1b801 100644 --- a/homeassistant/components/august/translations/es-419.json +++ b/homeassistant/components/august/translations/es-419.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, @@ -12,7 +16,8 @@ "timeout": "Tiempo de espera (segundos)", "username": "Nombre de usuario" }, - "description": "Si el M\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', Nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', Nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'." + "description": "Si el M\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', Nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', Nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'.", + "title": "Configurar una cuenta de August" }, "validation": { "data": { diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json index 28d6ed8842e..dce916bb788 100644 --- a/homeassistant/components/august/translations/ko.json +++ b/homeassistant/components/august/translations/ko.json @@ -17,7 +17,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 'phone'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", - "title": "August \uacc4\uc815 \uc124\uc815" + "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30" }, "validation": { "data": { diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json new file mode 100644 index 00000000000..1697f634d9a --- /dev/null +++ b/homeassistant/components/august/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "login_method": "Aanmeldmethode", + "password": "Wachtwoord", + "timeout": "Time-out (seconden)", + "username": "Gebruikersnaam" + }, + "description": "Als de aanmeldingsmethode 'e-mail' is, is gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.", + "title": "Stel een augustus-account in" + }, + "validation": { + "data": { + "code": "Verificatiecode" + }, + "description": "Controleer je {login_method} ( {username} ) en voer de onderstaande verificatiecode in", + "title": "Tweestapsverificatie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index 2ba841ea139..838508f132d 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -23,8 +23,8 @@ "data": { "code": "Bekreftelseskode" }, - "description": "Kontroller {login_method} ({username}) og skriv inn bekreftelseskoden nedenfor", - "title": "To-faktor autentisering" + "description": "Kontroller {login_method} ({username}) og fyll inn bekreftelseskoden nedenfor", + "title": "Totrinnsbekreftelse" } } } diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json index 2798af40779..33ef6431792 100644 --- a/homeassistant/components/august/translations/pl.json +++ b/homeassistant/components/august/translations/pl.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { "login_method": "Metoda logowania", - "password": "Has\u0142o", + "password": "[%key_id:common::config_flow::data::password%]", "timeout": "Limit czasu (sekundy)", - "username": "Nazwa u\u017cytkownika" + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.", "title": "Konfiguracja konta August" diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json new file mode 100644 index 00000000000..df72f5daaf3 --- /dev/null +++ b/homeassistant/components/august/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "login_method": "Inloggningsmetod", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "validation": { + "title": "Tv\u00e5faktorsautentisering" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index e2f07276c6c..1d5a6e83ec1 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -7,7 +7,7 @@ from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([AuroraSensor(aurora_data, name)], True) -class AuroraSensor(BinarySensorDevice): +class AuroraSensor(BinarySensorEntity): """Implementation of an aurora sensor.""" def __init__(self, aurora_data, name): diff --git a/homeassistant/components/auth/translations/fi.json b/homeassistant/components/auth/translations/fi.json new file mode 100644 index 00000000000..b73c7e194f9 --- /dev/null +++ b/homeassistant/components/auth/translations/fi.json @@ -0,0 +1,15 @@ +{ + "mfa_setup": { + "notify": { + "step": { + "setup": { + "title": "Varmista asetukset" + } + }, + "title": "Ilmoita kertaluonteinen salasana" + }, + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/ko.json b/homeassistant/components/auth/translations/ko.json index be160b185ac..563c141587f 100644 --- a/homeassistant/components/auth/translations/ko.json +++ b/homeassistant/components/auth/translations/ko.json @@ -10,11 +10,11 @@ "step": { "init": { "description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", - "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815" + "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815\ud558\uae30" }, "setup": { "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:", - "title": "\uc124\uc815 \ud655\uc778" + "title": "\uc124\uc815 \ud655\uc778\ud558\uae30" } }, "title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc" @@ -26,7 +26,7 @@ "step": { "init": { "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", - "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" + "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131\ud558\uae30" } }, "title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)" diff --git a/homeassistant/components/auth/translations/no.json b/homeassistant/components/auth/translations/no.json index 48b5db8a3b6..ea0f1baa067 100644 --- a/homeassistant/components/auth/translations/no.json +++ b/homeassistant/components/auth/translations/no.json @@ -25,11 +25,11 @@ }, "step": { "init": { - "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/). \n\n {qr_code} \n \nEtter at du har skannet koden, skriver du inn den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du taste inn f\u00f8lgende kode manuelt: **`{code}`**.", - "title": "Konfigurer tofaktorautentisering ved hjelp av TOTP" + "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/).\n\n {qr_code} \n \nEtter at du har skannet koden, angir du den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du fylle inn f\u00f8lgende kode manuelt: **`{code}`**.", + "title": "Sett opp tofaktorautentisering ved hjelp av TOTP" } }, - "title": "TOTP" + "title": "" } } } \ No newline at end of file diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index ea6c1e81e66..e5b66594d2f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,4 +1,5 @@ """Allow to set up simple automation rules via the config file.""" +import asyncio import importlib import logging from typing import Any, Awaitable, Callable, List, Optional, Set @@ -54,6 +55,7 @@ CONF_SKIP_CONDITION = "skip_condition" CONDITION_USE_TRIGGER_VALUES = "use_trigger_values" CONDITION_TYPE_AND = "and" +CONDITION_TYPE_NOT = "not" CONDITION_TYPE_OR = "or" DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND @@ -126,13 +128,11 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: component = hass.data[DOMAIN] - results = [] - - for automation_entity in component.entities: - if entity_id in automation_entity.referenced_entities: - results.append(automation_entity.entity_id) - - return results + return [ + automation_entity.entity_id + for automation_entity in component.entities + if entity_id in automation_entity.referenced_entities + ] @callback @@ -159,13 +159,11 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: component = hass.data[DOMAIN] - results = [] - - for automation_entity in component.entities: - if device_id in automation_entity.referenced_devices: - results.append(automation_entity.entity_id) - - return results + return [ + automation_entity.entity_id + for automation_entity in component.entities + if device_id in automation_entity.referenced_devices + ] @callback @@ -442,26 +440,29 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self, home_assistant_start: bool ) -> Optional[Callable[[], None]]: """Set up the triggers.""" - removes = [] info = {"name": self._name, "home_assistant_start": home_assistant_start} + triggers = [] for conf in self._trigger_config: platform = importlib.import_module(f".{conf[CONF_PLATFORM]}", __name__) - remove = await platform.async_attach_trigger( # type: ignore - self.hass, conf, self.async_trigger, info + triggers.append( + platform.async_attach_trigger( # type: ignore + self.hass, conf, self.async_trigger, info + ) ) - if not remove: - _LOGGER.error("Error setting up trigger %s", self._name) - continue + results = await asyncio.gather(*triggers) - _LOGGER.info("Initialized trigger %s", self._name) - removes.append(remove) + if None in results: + _LOGGER.error("Error setting up trigger %s", self._name) + removes = [remove for remove in results if remove is not None] if not removes: return None + _LOGGER.info("Initialized trigger %s", self._name) + @callback def remove_triggers(): """Remove attached triggers.""" diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index d29a561f378..c2cd00fd683 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -36,17 +36,19 @@ async def async_validate_config_item(hass, config, full_config=None): config[CONF_TRIGGER] = triggers if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions + config[CONF_CONDITION] = await asyncio.gather( + *[ + condition.async_validate_condition_config(hass, cond) + for cond in config[CONF_CONDITION] + ] + ) - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions + config[CONF_ACTION] = await asyncio.gather( + *[ + script.async_validate_action_config(hass, action) + for action in config[CONF_ACTION] + ] + ) return config @@ -69,16 +71,18 @@ async def _try_async_validate_config_item(hass, config, full_config=None): async def async_validate_config(hass, config): """Validate config.""" - automations = [] validated_automations = await asyncio.gather( *( _try_async_validate_config_item(hass, p_config, config) for _, p_config in config_per_platform(config, DOMAIN) ) ) - for validated_automation in validated_automations: - if validated_automation is not None: - automations.append(validated_automation) + + automations = [ + validated_automation + for validated_automation in validated_automations + if validated_automation is not None + ] # Create a copy of the configuration with all config for current # component removed and add validated config back in. diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 12ffa29b962..5924cf3b809 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -85,9 +85,13 @@ async def async_attach_trigger(hass, config, action, automation_info): cancel_pressed_more_than() cancel_pressed_more_than = None held_time = dt_util.utcnow() - pressed_time - if held_less_than is not None and held_time < held_less_than: - if held_more_than is None or held_time > held_more_than: - hass.add_job(call_action) + + if ( + held_less_than is not None + and held_time < held_less_than + and (held_more_than is None or held_time > held_more_than) + ): + hass.add_job(call_action) hass.data["litejet_system"].on_switch_pressed(number, pressed) hass.data["litejet_system"].on_switch_released(number, released) diff --git a/homeassistant/components/automation/translations/no.json b/homeassistant/components/automation/translations/no.json index 2e6c49d8993..13f5ad1c642 100644 --- a/homeassistant/components/automation/translations/no.json +++ b/homeassistant/components/automation/translations/no.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, "title": "Automatisering" } \ No newline at end of file diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 14233d783f9..cae2a76dd03 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -47,10 +47,7 @@ async def async_attach_trigger(hass, config, action, automation_info): return zone_state = hass.states.get(zone_entity_id) - if from_s: - from_match = condition.zone(hass, zone_state, from_s) - else: - from_match = False + from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if ( diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index 92d66a554da..8f57fb08e96 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(AveaLight(bulb) for bulb in nearby_bulbs) -class AveaLight(Light): +class AveaLight(LightEntity): """Representation of an Avea.""" def __init__(self, light): diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 0c95b2bf736..e5281c13654 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import ( CONF_API_KEY, @@ -66,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(lights) -class AvionLight(Light): +class AvionLight(LightEntity): """Representation of an Avion light.""" def __init__(self, device): diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index d992c28746c..4709d706ad0 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_TRIGGER_TIME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class AxisBinarySensor(AxisEventBase, BinarySensorDevice): +class AxisBinarySensor(AxisEventBase, BinarySensorEntity): """Representation of a binary Axis event.""" def __init__(self, event, device): diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 6e8899c79d6..8a2530b2022 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==25"], "zeroconf": ["_axis-video._tcp.local."], - "codeowners": ["@kane610"] + "codeowners": ["@Kane610"] } diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 67a3bb0a49e..88b117a9802 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -5,10 +5,10 @@ "user": { "title": "Set up Axis device", "data": { - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port" + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -25,4 +25,4 @@ "not_axis_device": "Discovered device not an Axis device" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index ed822543a00..be048a510ed 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -2,7 +2,7 @@ from axis.event_stream import CLASS_OUTPUT -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class AxisSwitch(AxisEventBase, SwitchDevice): +class AxisSwitch(AxisEventBase, SwitchEntity): """Representation of a Axis switch.""" @property diff --git a/homeassistant/components/axis/translations/es-419.json b/homeassistant/components/axis/translations/es-419.json index a86131723e3..b5d1cb4ca7b 100644 --- a/homeassistant/components/axis/translations/es-419.json +++ b/homeassistant/components/axis/translations/es-419.json @@ -12,9 +12,11 @@ "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" diff --git a/homeassistant/components/axis/translations/fi.json b/homeassistant/components/axis/translations/fi.json new file mode 100644 index 00000000000..b81b9858cd0 --- /dev/null +++ b/homeassistant/components/axis/translations/fi.json @@ -0,0 +1,3 @@ +{ + "title": "Axis-laite" +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ko.json b/homeassistant/components/axis/translations/ko.json index ae2da8858b4..b336a290f9e 100644 --- a/homeassistant/components/axis/translations/ko.json +++ b/homeassistant/components/axis/translations/ko.json @@ -8,7 +8,7 @@ }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "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", "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, @@ -21,7 +21,7 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "Axis \uae30\uae30 \uc124\uc815" + "title": "Axis \uae30\uae30 \uc124\uc815\ud558\uae30" } } }, diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json index fb04b498d70..a316ca726b5 100644 --- a/homeassistant/components/axis/translations/no.json +++ b/homeassistant/components/axis/translations/no.json @@ -18,7 +18,7 @@ "data": { "host": "Vert", "password": "Passord", - "port": "", + "port": "Port", "username": "Brukernavn" }, "title": "Sett opp Axis enhet" diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json index 7473c62f668..4d4961bddb2 100644 --- a/homeassistant/components/axis/translations/pl.json +++ b/homeassistant/components/axis/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", "bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego", "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" @@ -16,10 +16,10 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika" + "host": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Konfiguracja urz\u0105dzenia Axis" } diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index b51653bb3c8..bd0fa797fd0 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -3,7 +3,7 @@ from collections import OrderedDict import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, @@ -91,8 +91,7 @@ def update_probability(prior, prob_given_true, prob_given_false): """Update probability using Bayes' rule.""" numerator = prob_given_true * prior denominator = numerator + prob_given_false * (1 - prior) - probability = numerator / denominator - return probability + return numerator / denominator async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -113,7 +112,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class BayesianBinarySensor(BinarySensorDevice): +class BayesianBinarySensor(BinarySensorEntity): """Representation of a Bayesian sensor.""" def __init__(self, name, prior, observations, probability_threshold, device_class): @@ -246,7 +245,7 @@ class BayesianBinarySensor(BinarySensorDevice): """Return True if numeric condition is met.""" entity = entity_observation["entity_id"] - should_trigger = condition.async_numeric_state( + return condition.async_numeric_state( self.hass, entity, entity_observation.get("below"), @@ -254,26 +253,18 @@ class BayesianBinarySensor(BinarySensorDevice): None, entity_observation, ) - return should_trigger def _process_state(self, entity_observation): """Return True if state conditions are met.""" entity = entity_observation["entity_id"] - should_trigger = condition.state( - self.hass, entity, entity_observation.get("to_state") - ) - - return should_trigger + return condition.state(self.hass, entity, entity_observation.get("to_state")) def _process_template(self, entity_observation): """Return True if template condition is True.""" template = entity_observation.get(CONF_VALUE_TEMPLATE) template.hass = self.hass - should_trigger = condition.async_template( - self.hass, template, entity_observation - ) - return should_trigger + return condition.async_template(self.hass, template, entity_observation) @property def name(self): @@ -299,9 +290,9 @@ class BayesianBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" - attr_observations_list = list( + attr_observations_list = [ obs.copy() for obs in self.current_observations.values() if obs is not None - ) + ] for item in attr_observations_list: item.pop("value_template", None) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py index b1245aeabde..229f7a6c61e 100644 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors) -class BBBGPIOBinarySensor(BinarySensorDevice): +class BBBGPIOBinarySensor(BinarySensorEntity): """Representation of a binary sensor that uses Beaglebone Black GPIO.""" def __init__(self, pin, params): diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 7dc5d958537..f022509f9de 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -15,6 +15,8 @@ from homeassistant.helpers.entity_component import EntityComponent # mypy: allow-untyped-defs, no-check-untyped-defs +_LOGGER = logging.getLogger(__name__) + DOMAIN = "binary_sensor" SCAN_INTERVAL = timedelta(seconds=30) @@ -142,7 +144,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class BinarySensorDevice(Entity): +class BinarySensorEntity(Entity): """Represent a binary sensor.""" @property @@ -159,3 +161,15 @@ class BinarySensorDevice(Entity): def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" return None + + +class BinarySensorDevice(BinarySensorEntity): + """Represent a binary sensor (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "BinarySensorDevice is deprecated, modify %s to extend BinarySensorEntity", + cls.__name__, + ) diff --git a/homeassistant/components/binary_sensor/translations/es-419.json b/homeassistant/components/binary_sensor/translations/es-419.json index 3954724934b..d8cc4219097 100644 --- a/homeassistant/components/binary_sensor/translations/es-419.json +++ b/homeassistant/components/binary_sensor/translations/es-419.json @@ -28,6 +28,7 @@ "is_not_occupied": "{entity_name} no est\u00e1 ocupado", "is_not_open": "{entity_name} est\u00e1 cerrado", "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} no tiene encendido", "is_not_present": "{entity_name} no est\u00e1 presente", "is_not_unsafe": "{entity_name} es seguro", "is_occupied": "{entity_name} est\u00e1 ocupado", @@ -68,19 +69,24 @@ "not_locked": "{entity_name} desbloqueado", "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} se desocup\u00f3", "not_opened": "{entity_name} cerrado", "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no encendido", "not_present": "{entity_name} no presente", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se ocup\u00f3", "opened": "{entity_name} abierto", "plugged_in": "{entity_name} enchufado", + "powered": "{entity_name} encendido", "present": "{entity_name} presente", "problem": "{entity_name} comenz\u00f3 a detectar problemas", "smoke": "{entity_name} comenz\u00f3 a detectar humo", "sound": "{entity_name} comenz\u00f3 a detectar sonido", "turned_off": "{entity_name} apagado", - "turned_on": "{entity_name} encendido" + "turned_on": "{entity_name} encendido", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} comenz\u00f3 a detectar vibraciones" } }, "state": { diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index b0bb47b77d8..95a9778345e 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -115,8 +115,8 @@ "on": "Aperta" }, "gas": { - "off": "Assente", - "on": "Rilevato" + "off": "Assenza", + "on": "Presenza" }, "heat": { "off": "Normale", @@ -131,16 +131,16 @@ "on": "Bagnato" }, "motion": { - "off": "Assente", - "on": "Rilevato" + "off": "Assenza", + "on": "Presenza" }, "occupancy": { - "off": "Vuoto", - "on": "Rilevato" + "off": "Assenza", + "on": "Presenza" }, "opening": { "off": "Chiuso", - "on": "Aperta" + "on": "Aperto" }, "presence": { "off": "Fuori casa", @@ -155,15 +155,15 @@ "on": "Non Sicuro" }, "smoke": { - "off": "Assente", - "on": "Rilevato" + "off": "Assenza", + "on": "Presenza" }, "sound": { - "off": "Assente", - "on": "Rilevato" + "off": "Assenza", + "on": "Presenza" }, "vibration": { - "off": "Assente", + "off": "Assenza", "on": "Rilevata" }, "window": { diff --git a/homeassistant/components/binary_sensor/translations/ko.json b/homeassistant/components/binary_sensor/translations/ko.json index cd7281cbbb0..0b8ef0b73d5 100644 --- a/homeassistant/components/binary_sensor/translations/ko.json +++ b/homeassistant/components/binary_sensor/translations/ko.json @@ -2,10 +2,10 @@ "device_automation": { "condition_type": { "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", - "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6b0\uba74", + "is_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc73c\uba74", "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74", "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uba74", - "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6b0\uba74", + "is_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc73c\uba74", "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uba74", "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud558\uba74", @@ -19,9 +19,9 @@ "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74", - "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\ub2e4\uba74", + "is_not_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uc73c\uba74", "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\ub2e4\uba74", - "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\ub2e4\uba74", + "is_not_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uc73c\uba74", "is_not_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud558\uba74", "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc73c\uba74", @@ -46,10 +46,10 @@ }, "trigger_type": { "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c", - "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9c8 \ub54c", + "cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc544\uc84c\uc744 \ub54c", "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub420 \ub54c", "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud560 \ub54c", - "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9c8 \ub54c", + "hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc544\uc84c\uc744 \ub54c", "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud560 \ub54c", "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", @@ -63,9 +63,9 @@ "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", "not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub420 \ub54c", - "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uac8c \ub410\uc744 \ub54c", "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9c8 \ub54c", - "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uac8c \ub410\uc744 \ub54c", "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c", "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9c8 \ub54c", "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc744 \ub54c", diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 1264e770ce4..b78a50a8628 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -90,31 +90,85 @@ } }, "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + }, "battery": { "off": "Normalt", "on": "Lavt" }, "cold": { + "off": "", "on": "Kald" }, + "connectivity": { + "off": "Frakoblet", + "on": "Tilkoblet" + }, + "door": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "garage_door": { + "off": "Lukket", + "on": "\u00c5pen" + }, "gas": { "off": "Klar", "on": "Oppdaget" }, "heat": { + "off": "", "on": "Varm" }, + "lock": { + "off": "L\u00e5st", + "on": "Ul\u00e5st" + }, "moisture": { "off": "T\u00f8rr", "on": "V\u00e5t" }, + "motion": { + "off": "Klart", + "on": "Oppdaget" + }, + "occupancy": { + "off": "Klart", + "on": "Oppdaget" + }, + "opening": { + "off": "Lukket", + "on": "\u00c5pen" + }, + "presence": { + "off": "Borte", + "on": "Hjemme" + }, "problem": { - "off": "OK", - "on": "Problem" + "off": "", + "on": "" }, "safety": { "off": "Sikker", "on": "Usikker" + }, + "smoke": { + "off": "Klart", + "on": "Oppdaget" + }, + "sound": { + "off": "Klart", + "on": "Oppdaget" + }, + "vibration": { + "off": "Klart", + "on": "Oppdaget" + }, + "window": { + "off": "Lukket", + "on": "\u00c5pen" } }, "title": "Bin\u00e6r sensor" diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index a0ea369bb9b..9ae696a5276 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -6,7 +6,7 @@ from pyblackbird import get_blackbird from serial import SerialException import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -128,7 +128,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class BlackbirdZone(MediaPlayerDevice): +class BlackbirdZone(MediaPlayerEntity): """Representation of a Blackbird matrix zone.""" def __init__(self, blackbird, sources, zone_id, zone_name): diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py new file mode 100644 index 00000000000..1196deb27b7 --- /dev/null +++ b/homeassistant/components/blebox/__init__.py @@ -0,0 +1,122 @@ +"""The BleBox devices integration.""" +import asyncio +import logging + +from blebox_uniapi.error import Error +from blebox_uniapi.products import Products +from blebox_uniapi.session import ApiHost + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity + +from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["cover", "sensor"] + +PARALLEL_UPDATES = 0 + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the BleBox devices component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up BleBox devices from a config entry.""" + + websession = async_get_clientsession(hass) + + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + timeout = DEFAULT_SETUP_TIMEOUT + + api_host = ApiHost(host, port, timeout, websession, hass.loop) + + try: + product = await Products.async_from_host(api_host) + except Error as ex: + _LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex) + raise ConfigEntryNotReady from ex + + domain = hass.data.setdefault(DOMAIN, {}) + domain_entry = domain.setdefault(entry.entry_id, {}) + product = domain_entry.setdefault(PRODUCT, product) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +@callback +def create_blebox_entities(product, async_add, entity_klass, entity_type): + """Create entities from a BleBox product's features.""" + + entities = [] + if entity_type in product.features: + for feature in product.features[entity_type]: + entities.append(entity_klass(feature)) + + async_add(entities, True) + + +class BleBoxEntity(Entity): + """Implements a common class for entities representing a BleBox feature.""" + + def __init__(self, feature): + """Initialize a BleBox entity.""" + self._feature = feature + + @property + def name(self): + """Return the internal entity name.""" + return self._feature.full_name + + @property + def unique_id(self): + """Return a unique id.""" + return self._feature.unique_id + + async def async_update(self): + """Update the entity state.""" + try: + await self._feature.async_update() + except Error as ex: + _LOGGER.error("Updating '%s' failed: %s", self.name, ex) + + @property + def device_info(self): + """Return device information for this entity.""" + product = self._feature.product + return { + "identifiers": {(DOMAIN, product.unique_id)}, + "name": product.name, + "manufacturer": product.brand, + "model": product.model, + "sw_version": product.firmware_version, + } diff --git a/homeassistant/components/blebox/config_flow.py b/homeassistant/components/blebox/config_flow.py new file mode 100644 index 00000000000..1c73346ddf9 --- /dev/null +++ b/homeassistant/components/blebox/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for BleBox devices integration.""" +import logging + +from blebox_uniapi.error import Error, UnsupportedBoxVersion +from blebox_uniapi.products import Products +from blebox_uniapi.session import ApiHost +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 ( + ADDRESS_ALREADY_CONFIGURED, + CANNOT_CONNECT, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SETUP_TIMEOUT, + DOMAIN, + UNKNOWN, + UNSUPPORTED_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +def host_port(data): + """Return a list with host and port.""" + return (data[CONF_HOST], data[CONF_PORT]) + + +def create_schema(previous_input=None): + """Create a schema with given values as default.""" + if previous_input is not None: + host, port = host_port(previous_input) + else: + host = DEFAULT_HOST + port = DEFAULT_PORT + + return vol.Schema( + { + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_PORT, default=port): int, + } + ) + + +LOG_MSG = { + UNSUPPORTED_VERSION: "Outdated firmware", + CANNOT_CONNECT: "Failed to identify device", + UNKNOWN: "Unknown error while identifying device", +} + + +class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for BleBox devices.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the BleBox config flow.""" + self.device_config = {} + + def handle_step_exception( + self, step, exception, schema, host, port, message_id, log_fn + ): + """Handle step exceptions.""" + + log_fn("%s at %s:%d (%s)", LOG_MSG[message_id], host, port, exception) + + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={"base": message_id}, + description_placeholders={"address": f"{host}:{port}"}, + ) + + async def async_step_user(self, user_input=None): + """Handle initial user-triggered config step.""" + + hass = self.hass + schema = create_schema(user_input) + + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={}, + description_placeholders={}, + ) + + addr = host_port(user_input) + + for entry in hass.config_entries.async_entries(DOMAIN): + if addr == host_port(entry.data): + host, port = addr + return self.async_abort( + reason=ADDRESS_ALREADY_CONFIGURED, + description_placeholders={"address": f"{host}:{port}"}, + ) + + websession = async_get_clientsession(hass) + api_host = ApiHost(*addr, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER) + + try: + product = await Products.async_from_host(api_host) + + except UnsupportedBoxVersion as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, UNSUPPORTED_VERSION, _LOGGER.debug + ) + + except Error as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, CANNOT_CONNECT, _LOGGER.warning + ) + + except RuntimeError as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, UNKNOWN, _LOGGER.error + ) + + # Check if configured but IP changed since + await self.async_set_unique_id(product.unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=product.name, data=user_input) diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py new file mode 100644 index 00000000000..71d2193f904 --- /dev/null +++ b/homeassistant/components/blebox/const.py @@ -0,0 +1,49 @@ +"""Constants for the BleBox devices integration.""" + +from homeassistant.components.cover import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHUTTER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS + +DOMAIN = "blebox" +PRODUCT = "product" + +DEFAULT_SETUP_TIMEOUT = 3 + +# translation strings +ADDRESS_ALREADY_CONFIGURED = "address_already_configured" +CANNOT_CONNECT = "cannot_connect" +UNSUPPORTED_VERSION = "unsupported_version" +UNKNOWN = "unknown" + +BLEBOX_TO_HASS_DEVICE_CLASSES = { + "shutter": DEVICE_CLASS_SHUTTER, + "gatebox": DEVICE_CLASS_DOOR, + "gate": DEVICE_CLASS_GATE, +} + +BLEBOX_TO_HASS_COVER_STATES = { + None: None, + 0: STATE_CLOSING, # moving down + 1: STATE_OPENING, # moving up + 2: STATE_OPEN, # manually stopped + 3: STATE_CLOSED, # lower limit + 4: STATE_OPEN, # upper limit / open + # gateController + 5: STATE_OPEN, # overload + 6: STATE_OPEN, # motor failure + # 7 is not used + 8: STATE_OPEN, # safety stop +} + +BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} +BLEBOX_DEV_CLASS_MAP = {"temperature": DEVICE_CLASS_TEMPERATURE} + +DEFAULT_HOST = "192.168.0.2" +DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py new file mode 100644 index 00000000000..2a8f0219267 --- /dev/null +++ b/homeassistant/components/blebox/cover.py @@ -0,0 +1,97 @@ +"""BleBox cover entity.""" + +from homeassistant.components.cover import ( + ATTR_POSITION, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) + +from . import BleBoxEntity, create_blebox_entities +from .const import ( + BLEBOX_TO_HASS_COVER_STATES, + BLEBOX_TO_HASS_DEVICE_CLASSES, + DOMAIN, + PRODUCT, +) + + +async def async_setup_entry(hass, config_entry, async_add): + """Set up a BleBox entry.""" + + product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] + create_blebox_entities(product, async_add, BleBoxCoverEntity, "covers") + return True + + +class BleBoxCoverEntity(BleBoxEntity, CoverEntity): + """Representation of a BleBox cover feature.""" + + @property + def state(self): + """Return the equivalent HA cover state.""" + return BLEBOX_TO_HASS_COVER_STATES[self._feature.state] + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + + @property + def supported_features(self): + """Return the supported cover features.""" + position = SUPPORT_SET_POSITION if self._feature.is_slider else 0 + stop = SUPPORT_STOP if self._feature.has_stop else 0 + + return position | stop | SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def current_cover_position(self): + """Return the current cover position.""" + position = self._feature.current + if position == -1: # possible for shutterBox + return None + + return None if position is None else 100 - position + + @property + def is_opening(self): + """Return whether cover is opening.""" + return self._is_state(STATE_OPENING) + + @property + def is_closing(self): + """Return whether cover is closing.""" + return self._is_state(STATE_CLOSING) + + @property + def is_closed(self): + """Return whether cover is closed.""" + return self._is_state(STATE_CLOSED) + + async def async_open_cover(self, **kwargs): + """Open the cover position.""" + await self._feature.async_open() + + async def async_close_cover(self, **kwargs): + """Close the cover position.""" + await self._feature.async_close() + + async def async_set_cover_position(self, **kwargs): + """Set the cover position.""" + + position = kwargs[ATTR_POSITION] + await self._feature.async_set_position(100 - position) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._feature.async_stop() + + def _is_state(self, state_name): + value = self.state + return None if value is None else value == state_name diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json new file mode 100644 index 00000000000..703d9042270 --- /dev/null +++ b/homeassistant/components/blebox/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "blebox", + "name": "BleBox devices", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/blebox", + "requirements": ["blebox_uniapi==1.3.2"], + "codeowners": [ "@gadgetmobile" ] +} diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py new file mode 100644 index 00000000000..b43b4f21da5 --- /dev/null +++ b/homeassistant/components/blebox/sensor.py @@ -0,0 +1,33 @@ +"""BleBox sensor entities.""" + +from homeassistant.helpers.entity import Entity + +from . import BleBoxEntity, create_blebox_entities +from .const import BLEBOX_DEV_CLASS_MAP, BLEBOX_TO_UNIT_MAP, DOMAIN, PRODUCT + + +async def async_setup_entry(hass, config_entry, async_add): + """Set up a BleBox entry.""" + + product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] + create_blebox_entities(product, async_add, BleBoxSensorEntity, "sensors") + return True + + +class BleBoxSensorEntity(BleBoxEntity, Entity): + """Representation of a BleBox sensor feature.""" + + @property + def state(self): + """Return the state.""" + return self._feature.current + + @property + def unit_of_measurement(self): + """Return the unit.""" + return BLEBOX_TO_UNIT_MAP[self._feature.unit] + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_DEV_CLASS_MAP[self._feature.device_class] diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json new file mode 100644 index 00000000000..f929d62d8d9 --- /dev/null +++ b/homeassistant/components/blebox/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This BleBox device is already configured.", + "address_already_configured": "A BleBox device is already configured at {address}." + }, + "error": { + "cannot_connect": "Unable to connect to the BleBox device. (Check the logs for errors.)", + "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first.", + "unknown": "Unknown error while connecting to the BleBox device. (Check the logs for errors.)" + }, + "flow_title": "BleBox device: {name} ({host})", + "step": { + "user": { + "description": "Set up your BleBox to integrate with Home Assistant.", + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "title": "Set up your BleBox device" + } + } + } +} diff --git a/homeassistant/components/blebox/translations/ca.json b/homeassistant/components/blebox/translations/ca.json new file mode 100644 index 00000000000..39bf371fac1 --- /dev/null +++ b/homeassistant/components/blebox/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ja hi ha un dispositiu BleBox configurat a {address}.", + "already_configured": "Aquest dispositiu BleBox ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar al dispositiu BleBox. (Consulta els registres per veure-hi els errors).", + "unknown": "S'ha produ\u00eft un error desconegut en connectar-se al dispositiu BleBox. (Consulta els registres per veure-hi els errors).", + "unsupported_version": "El dispositiu BleBox t\u00e9 un firmware obsolet. Primer actualitza'l." + }, + "flow_title": "Dispositiu BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Adre\u00e7a IP", + "port": "Port" + }, + "description": "Configura el teu dispositiu BleBox per a integrar-lo a Home Assistant.", + "title": "Configuraci\u00f3 del dispositiu BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json new file mode 100644 index 00000000000..501b1335244 --- /dev/null +++ b/homeassistant/components/blebox/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ein BleBox-Ger\u00e4t ist bereits unter {address} konfiguriert.", + "already_configured": "Dieses BleBox-Ger\u00e4t ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung mit dem BleBox-Ger\u00e4t nicht m\u00f6glich. (\u00dcberpr\u00fcfen Sie die Protokolle auf Fehler).", + "unknown": "Unbekannter Fehler beim Anschlie\u00dfen an das BleBox-Ger\u00e4t. (Pr\u00fcfen Sie die Protokolle auf Fehler).", + "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst." + }, + "flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )", + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Richten Sie Ihre BleBox f\u00fcr die Integration mit dem Home Assistant ein.", + "title": "Richten Sie Ihr BleBox-Ger\u00e4t ein" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/en.json b/homeassistant/components/blebox/translations/en.json new file mode 100644 index 00000000000..fef3d9ceac7 --- /dev/null +++ b/homeassistant/components/blebox/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "A BleBox device is already configured at {address}.", + "already_configured": "This BleBox device is already configured." + }, + "error": { + "cannot_connect": "Unable to connect to the BleBox device. (Check the logs for errors.)", + "unknown": "Unknown error while connecting to the BleBox device. (Check the logs for errors.)", + "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first." + }, + "flow_title": "BleBox device: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP address", + "port": "Port" + }, + "description": "Set up your BleBox to integrate with Home Assistant.", + "title": "Set up your BleBox device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/es.json b/homeassistant/components/blebox/translations/es.json new file mode 100644 index 00000000000..ceed1592992 --- /dev/null +++ b/homeassistant/components/blebox/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un dispositivo BleBox ya est\u00e1 configurado en {address}.", + "already_configured": "Este dispositivo BleBox ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar con el dispositivo BleBox. (Comprueba los logs en busca de errores).", + "unknown": "Error desconocido al conectarse con el dispositivo BleBox. (Comprueba los logs en busca de errores).", + "unsupported_version": "El dispositivo BleBox tiene un firmware anticuado. Por favor, actual\u00edzalo primero." + }, + "flow_title": "Dispositivo BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "port": "Puerto" + }, + "description": "Configura tu BleBox para integrarse con Home Assistant.", + "title": "Configura tu dispositivo BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/fi.json b/homeassistant/components/blebox/translations/fi.json new file mode 100644 index 00000000000..d99fac1fdf3 --- /dev/null +++ b/homeassistant/components/blebox/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/fr.json b/homeassistant/components/blebox/translations/fr.json new file mode 100644 index 00000000000..d2f43a2c328 --- /dev/null +++ b/homeassistant/components/blebox/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un p\u00e9riph\u00e9rique BleBox est d\u00e9j\u00e0 configur\u00e9 \u00e0 {address}.", + "already_configured": "Ce p\u00e9riph\u00e9rique BleBox est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de connecter le p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)", + "unknown": "Erreur inconnue lors de la connexion au p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)" + }, + "flow_title": "P\u00e9riph\u00e9rique Blebox: {name} ({host)}", + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Configurez votre BleBox pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Configurer votre appareil BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/he.json b/homeassistant/components/blebox/translations/he.json new file mode 100644 index 00000000000..001f8457f14 --- /dev/null +++ b/homeassistant/components/blebox/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json new file mode 100644 index 00000000000..a4c3e1bd2d3 --- /dev/null +++ b/homeassistant/components/blebox/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-vel rendelkezik. El\u0151sz\u00f6r friss\u00edtse." + }, + "flow_title": "BleBox eszk\u00f6z: {name} ({host})", + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/it.json b/homeassistant/components/blebox/translations/it.json new file mode 100644 index 00000000000..6b36ef97a51 --- /dev/null +++ b/homeassistant/components/blebox/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un dispositivo BleBox \u00e8 gi\u00e0 configurato in {address}.", + "already_configured": "Questo dispositivo BleBox \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi al dispositivo BleBox. (Controllare i registri per errori).", + "unknown": "Errore sconosciuto durante la connessione al dispositivo BleBox. (Controllare i registri per errori).", + "unsupported_version": "Il dispositivo BleBox ha un firmware obsoleto. Si prega di aggiornarlo prima." + }, + "flow_title": "Dispositivo BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "port": "Porta" + }, + "description": "Configura BleBox per l'integrazione con Home Assistant.", + "title": "Configura il tuo dispositivo BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/ko.json b/homeassistant/components/blebox/translations/ko.json new file mode 100644 index 00000000000..ff3fa740092 --- /dev/null +++ b/homeassistant/components/blebox/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "BleBox \uae30\uae30\uac00 {address} \ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured": "BleBox \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "BleBox \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. (\ub85c\uadf8\uc5d0\uc11c \uc624\ub958 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694.)", + "unknown": "BleBox \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. (\ub85c\uadf8\uc5d0\uc11c \uc624\ub958 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694.)", + "unsupported_version": "BleBox \uae30\uae30 \ud38c\uc6e8\uc5b4\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc8fc\uc138\uc694." + }, + "flow_title": "BleBox \uae30\uae30: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + }, + "description": "Home Assistant \uc5d0 BleBox \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "BleBox \uae30\uae30 \uc124\uc815\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/lb.json b/homeassistant/components/blebox/translations/lb.json new file mode 100644 index 00000000000..dae53828764 --- /dev/null +++ b/homeassistant/components/blebox/translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ee BleBox Apparat ass scho konfigur\u00e9iert op {adress}.", + "already_configured": "D\u00ebse BleBox Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Keng Verbindung mam BleBox Apparat m\u00e9iglech. (Kuck Logs fir Feeler.)", + "unknown": "Onbekannte Feeler beim verbannen mam BleBox Apparat. (Kuck Logs fir Feeler.)", + "unsupported_version": "BleBox Apparat huet eng aal Firmware. Maach fir d'\u00e9ischt d'Mise \u00e0 jour." + }, + "flow_title": "BleBox Apparat: {name} ({host})", + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "BleBox ariichten fir d'Integratioun mam Home Assistant.", + "title": "BleBox Apparat ariichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/no.json b/homeassistant/components/blebox/translations/no.json new file mode 100644 index 00000000000..ff6073410ac --- /dev/null +++ b/homeassistant/components/blebox/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "En BleBox-enhet er allerede konfigurert p\u00e5 {address} .", + "already_configured": "Denne BleBox-enheten er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kan ikke koble til BleBox-enheten. (Kontroller loggene for feil.)", + "unknown": "Ukjent feil under tilkobling til BleBox-enheten. (Kontroller loggene for feil.)", + "unsupported_version": "BleBox-enheten har utdatert fastvare. Vennligst oppgrader den f\u00f8rst." + }, + "flow_title": "BleBox-enhet: {name} ({host})", + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.", + "title": "Konfigurere BleBox-enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json new file mode 100644 index 00000000000..f158ad2c75d --- /dev/null +++ b/homeassistant/components/blebox/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Urz\u0105dzenie BleBox jest ju\u017c skonfigurowane pod adresem {address}.", + "already_configured": "To urz\u0105dzenie BleBox jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem BleBox. (Sprawd\u017a logi pod k\u0105tem b\u0142\u0119d\u00f3w).", + "unknown": "Nieznany b\u0142\u0105d podczas \u0142\u0105czenia z urz\u0105dzeniem BleBox. (Sprawd\u017a logi pod k\u0105tem b\u0142\u0119d\u00f3w).", + "unsupported_version": "Urz\u0105dzenie BleBox ma nieaktualny firmware. Prosz\u0119 go najpierw zaktualizowa\u0107." + }, + "flow_title": "Urz\u0105dzenie BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "[%key_id:common::config_flow::data::ip%]", + "port": "[%key_id:common::config_flow::data::port%]" + }, + "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistant.", + "title": "Skonfiguruj urz\u0105dzenie BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/ru.json b/homeassistant/components/blebox/translations/ru.json new file mode 100644 index 00000000000..b9374cfb11f --- /dev/null +++ b/homeassistant/components/blebox/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {address } \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u043d\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.", + "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u043d\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.", + "unsupported_version": "\u041f\u0440\u043e\u0448\u0438\u0432\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0435\u0451." + }, + "flow_title": "BleBox device: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BleBox.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/sl.json b/homeassistant/components/blebox/translations/sl.json new file mode 100644 index 00000000000..f34d8d57a18 --- /dev/null +++ b/homeassistant/components/blebox/translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Naprava BleBox je \u017ee konfigurirana na {address} .", + "already_configured": "Ta naprava BleBox je \u017ee nastavljena." + }, + "error": { + "cannot_connect": "Ni mogo\u010de povezati z napravo BleBox. (Za napako preverite dnevnik)", + "unknown": "Neznana napaka med povezovanjem z napravo BleBox. (Za napako preverite dnevnik)", + "unsupported_version": "Naprava BleBox ima zastarelo programsko opremo. Najprej jo nadgradite." + }, + "flow_title": "Naprava BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "port": "Vrata" + }, + "description": "Nastavite svoj BleBox za integracijo s Home Assistant.", + "title": "Nastavite napravo BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/zh-Hant.json b/homeassistant/components/blebox/translations/zh-Hant.json new file mode 100644 index 00000000000..75f6249a138 --- /dev/null +++ b/homeassistant/components/blebox/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "already_configured": "BleBox \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 BleBox \u8a2d\u5099\uff08\u8acb\u53c3\u95b1\u65e5\u8a8c\uff09\u3002", + "unknown": "\u9023\u7dda\u81f3 BleBox \u8a2d\u5099\u767c\u751f\u672a\u77e5\u932f\u8aa4\uff08\u8acb\u53c3\u95b1\u65e5\u8a8c\uff09\u3002", + "unsupported_version": "BleBox \u8a2d\u5099\u97cc\u9ad4\u904e\u820a\uff0c\u8acb\u5148\u9032\u884c\u66f4\u65b0\u3002" + }, + "flow_title": "BleBox \u8a2d\u5099\uff1a{name} ({host})", + "step": { + "user": { + "data": { + "host": "IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a BleBox \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u8a2d\u5b9a BleBox \u8a2d\u5099" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index e233a8b21d8..3576574c357 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,81 +1,40 @@ """Support for Blink Home Camera System.""" -from datetime import timedelta +import asyncio import logging -from blinkpy import blinkpy +from blinkpy.blinkpy import Blink import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - CONF_BINARY_SENSORS, CONF_FILENAME, - CONF_MODE, - CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_OFFSET, CONF_PASSWORD, + CONF_PIN, CONF_SCAN_INTERVAL, - CONF_SENSORS, CONF_USERNAME, - TEMP_FAHRENHEIT, ) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import config_validation as cv + +from .const import ( + DEFAULT_OFFSET, + DEFAULT_SCAN_INTERVAL, + DEVICE_ID, + DOMAIN, + PLATFORMS, + SERVICE_REFRESH, + SERVICE_SAVE_VIDEO, + SERVICE_SEND_PIN, + SERVICE_TRIGGER, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = "blink" -BLINK_DATA = "blink" - -CONF_CAMERA = "camera" -CONF_ALARM_CONTROL_PANEL = "alarm_control_panel" - -DEFAULT_BRAND = "Blink" -DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" -SIGNAL_UPDATE_BLINK = "blink_update" - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=300) - -TYPE_CAMERA_ARMED = "motion_enabled" -TYPE_MOTION_DETECTED = "motion_detected" -TYPE_TEMPERATURE = "temperature" -TYPE_BATTERY = "battery" -TYPE_WIFI_STRENGTH = "wifi_strength" - -SERVICE_REFRESH = "blink_update" -SERVICE_TRIGGER = "trigger_camera" -SERVICE_SAVE_VIDEO = "save_video" - -BINARY_SENSORS = { - TYPE_CAMERA_ARMED: ["Camera Armed", "mdi:verified"], - TYPE_MOTION_DETECTED: ["Motion Detected", "mdi:run-fast"], -} - -SENSORS = { - TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, "mdi:thermometer"], - TYPE_BATTERY: ["Battery", "", "mdi:battery-80"], - TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", "mdi:wifi-strength-2"], -} - -BINARY_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] - ) - } -) - -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ) - } -) - SERVICE_TRIGGER_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) - SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string} ) +SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string}) CONFIG_SCHEMA = vol.Schema( { @@ -83,13 +42,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_OFFSET, default=1): int, - vol.Optional(CONF_MODE, default=""): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int, } ) }, @@ -97,61 +50,127 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up Blink System.""" - - conf = config[BLINK_DATA] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - scan_interval = conf[CONF_SCAN_INTERVAL] - is_legacy = bool(conf[CONF_MODE] == "legacy") - motion_interval = conf[CONF_OFFSET] - hass.data[BLINK_DATA] = blinkpy.Blink( - username=username, - password=password, - motion_interval=motion_interval, - legacy_subdomain=is_legacy, +def _blink_startup_wrapper(entry): + """Startup wrapper for blink.""" + blink = Blink( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + motion_interval=DEFAULT_OFFSET, + legacy_subdomain=False, + no_prompt=True, + device_id=DEVICE_ID, ) - hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds() - hass.data[BLINK_DATA].start() + blink.refresh_rate = entry.data[CONF_SCAN_INTERVAL] - platforms = [ - ("alarm_control_panel", {}), - ("binary_sensor", conf[CONF_BINARY_SENSORS]), - ("camera", {}), - ("sensor", conf[CONF_SENSORS]), - ] + try: + blink.login_response = entry.data["login_response"] + blink.setup_params(entry.data["login_response"]) + except KeyError: + blink.get_auth_token() - for component, schema in platforms: - discovery.load_platform(hass, component, DOMAIN, schema, config) + blink.setup_params(entry.data["login_response"]) + blink.setup_post_verify() + return blink - def trigger_camera(call): - """Trigger a camera.""" - cameras = hass.data[BLINK_DATA].cameras - name = call.data[CONF_NAME] - if name in cameras: - cameras[name].snap_picture() - hass.data[BLINK_DATA].refresh(force_cache=True) - def blink_refresh(event_time): - """Call blink to refresh info.""" - hass.data[BLINK_DATA].refresh(force_cache=True) +async def async_setup(hass, config): + """Set up a config entry.""" + hass.data[DOMAIN] = {} + if DOMAIN not in config: + return True - async def async_save_video(call): - """Call save video service handler.""" - await async_handle_save_video_service(hass, call) + conf = config.get(DOMAIN, {}) + + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) - hass.services.register(DOMAIN, SERVICE_REFRESH, blink_refresh) - hass.services.register( - DOMAIN, SERVICE_TRIGGER, trigger_camera, schema=SERVICE_TRIGGER_SCHEMA - ) - hass.services.register( - DOMAIN, SERVICE_SAVE_VIDEO, async_save_video, schema=SERVICE_SAVE_VIDEO_SCHEMA - ) return True -async def async_handle_save_video_service(hass, call): +async def async_setup_entry(hass, entry): + """Set up Blink via config entry.""" + hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( + _blink_startup_wrapper, entry + ) + + if not hass.data[DOMAIN][entry.entry_id].available: + _LOGGER.error("Blink unavailable for setup") + return False + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + def trigger_camera(call): + """Trigger a camera.""" + cameras = hass.data[DOMAIN][entry.entry_id].cameras + name = call.data[CONF_NAME] + if name in cameras: + cameras[name].snap_picture() + blink_refresh() + + def blink_refresh(event_time=None): + """Call blink to refresh info.""" + hass.data[DOMAIN][entry.entry_id].refresh(force_cache=True) + + async def async_save_video(call): + """Call save video service handler.""" + await async_handle_save_video_service(hass, entry, call) + + def send_pin(call): + """Call blink to send new pin.""" + pin = call.data[CONF_PIN] + hass.data[DOMAIN][entry.entry_id].login_handler.send_auth_key( + hass.data[DOMAIN][entry.entry_id], pin, + ) + + hass.services.async_register(DOMAIN, SERVICE_REFRESH, blink_refresh) + hass.services.async_register( + DOMAIN, SERVICE_TRIGGER, trigger_camera, schema=SERVICE_TRIGGER_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_SAVE_VIDEO, async_save_video, schema=SERVICE_SAVE_VIDEO_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_SEND_PIN, send_pin, schema=SERVICE_SEND_PIN_SCHEMA + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload Blink entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if not unload_ok: + return False + + hass.data[DOMAIN].pop(entry.entry_id) + + if len(hass.data[DOMAIN]) != 0: + return True + + hass.services.async_remove(DOMAIN, SERVICE_REFRESH) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER) + hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO_SCHEMA) + hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN) + + return True + + +async def async_handle_save_video_service(hass, entry, call): """Handle save video service calls.""" camera_name = call.data[CONF_NAME] video_path = call.data[CONF_FILENAME] @@ -161,7 +180,7 @@ async def async_handle_save_video_service(hass, call): def _write_video(camera_name, video_path): """Call video write.""" - all_cameras = hass.data[BLINK_DATA].cameras + all_cameras = hass.data[DOMAIN][entry.entry_id].cameras if camera_name in all_cameras: all_cameras[camera_name].video_to_file(video_path) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index 9b23c1606d4..1ca4c4beac9 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -1,7 +1,7 @@ """Support for Blink Alarm Control Panel.""" import logging -from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -9,26 +9,24 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from . import BLINK_DATA, DEFAULT_ATTRIBUTION +from .const import DEFAULT_ATTRIBUTION, DOMAIN _LOGGER = logging.getLogger(__name__) ICON = "mdi:security" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Arlo Alarm Control Panels.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] +async def async_setup_entry(hass, config, async_add_entities): + """Set up the Blink Alarm Control Panels.""" + data = hass.data[DOMAIN][config.entry_id] sync_modules = [] for sync_name, sync_module in data.sync.items(): sync_modules.append(BlinkSyncModule(data, sync_name, sync_module)) - add_entities(sync_modules, True) + async_add_entities(sync_modules) -class BlinkSyncModule(AlarmControlPanel): +class BlinkSyncModule(AlarmControlPanelEntity): """Representation of a Blink Alarm Control Panel.""" def __init__(self, data, name, sync): @@ -61,7 +59,7 @@ class BlinkSyncModule(AlarmControlPanel): @property def name(self): """Return the name of the panel.""" - return f"{BLINK_DATA} {self._name}" + return f"{DOMAIN} {self._name}" @property def device_state_attributes(self): diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index e8c01953bff..8c86622b74e 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,24 +1,26 @@ """Support for Blink system camera control.""" -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.components.binary_sensor import BinarySensorEntity -from . import BINARY_SENSORS, BLINK_DATA +from .const import DOMAIN, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED + +BINARY_SENSORS = { + TYPE_CAMERA_ARMED: ["Camera Armed", "mdi:verified"], + TYPE_MOTION_DETECTED: ["Motion Detected", "mdi:run-fast"], +} -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config, async_add_entities): """Set up the blink binary sensors.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] + data = hass.data[DOMAIN][config.entry_id] - devs = [] + entities = [] for camera in data.cameras: - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: - devs.append(BlinkBinarySensor(data, camera, sensor_type)) - add_entities(devs, True) + for sensor_type in BINARY_SENSORS: + entities.append(BlinkBinarySensor(data, camera, sensor_type)) + async_add_entities(entities) -class BlinkBinarySensor(BinarySensorDevice): +class BlinkBinarySensor(BinarySensorEntity): """Representation of a Blink binary sensor.""" def __init__(self, data, camera, sensor_type): @@ -26,7 +28,7 @@ class BlinkBinarySensor(BinarySensorDevice): self.data = data self._type = sensor_type name, icon = BINARY_SENSORS[sensor_type] - self._name = f"{BLINK_DATA} {camera} {name}" + self._name = f"{DOMAIN} {camera} {name}" self._icon = icon self._camera = data.cameras[camera] self._state = None diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 52043324a40..c675d4dda56 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.camera import Camera -from . import BLINK_DATA, DEFAULT_BRAND +from .const import DEFAULT_BRAND, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -11,16 +11,14 @@ ATTR_VIDEO_CLIP = "video" ATTR_IMAGE = "image" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config, async_add_entities): """Set up a Blink Camera.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] - devs = [] + data = hass.data[DOMAIN][config.entry_id] + entities = [] for name, camera in data.cameras.items(): - devs.append(BlinkCamera(data, name, camera)) + entities.append(BlinkCamera(data, name, camera)) - add_entities(devs) + async_add_entities(entities) class BlinkCamera(Camera): @@ -30,7 +28,7 @@ class BlinkCamera(Camera): """Initialize a camera.""" super().__init__() self.data = data - self._name = f"{BLINK_DATA} {name}" + self._name = f"{DOMAIN} {name}" self._camera = camera self._unique_id = f"{camera.serial}-camera" self.response = None diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py new file mode 100644 index 00000000000..281dee17cb1 --- /dev/null +++ b/homeassistant/components/blink/config_flow.py @@ -0,0 +1,115 @@ +"""Config flow to configure Blink.""" +import logging + +from blinkpy.blinkpy import Blink +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PIN, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, blink): + """Validate the user input allows us to connect.""" + response = await hass.async_add_executor_job(blink.get_auth_token) + if not response: + raise InvalidAuth + if blink.key_required: + raise Require2FA + + return blink.login_response + + +class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Blink config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the blink flow.""" + self.blink = None + self.data = { + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + "login_response": None, + } + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + if user_input is not None: + self.data[CONF_USERNAME] = user_input["username"] + self.data[CONF_PASSWORD] = user_input["password"] + + await self.async_set_unique_id(self.data[CONF_USERNAME]) + + if CONF_SCAN_INTERVAL in user_input: + self.data[CONF_SCAN_INTERVAL] = user_input["scan_interval"] + + self.blink = Blink( + username=self.data[CONF_USERNAME], + password=self.data[CONF_PASSWORD], + motion_interval=DEFAULT_OFFSET, + legacy_subdomain=False, + no_prompt=True, + device_id=DEVICE_ID, + ) + + try: + response = await validate_input(self.hass, self.blink) + self.data["login_response"] = response + return self.async_create_entry(title=DOMAIN, data=self.data,) + except Require2FA: + return await self.async_step_2fa() + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + data_schema = { + vol.Required("username"): str, + vol.Required("password"): str, + } + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(data_schema), errors=errors, + ) + + async def async_step_2fa(self, user_input=None): + """Handle 2FA step.""" + if user_input is not None: + pin = user_input.get(CONF_PIN) + if await self.hass.async_add_executor_job( + self.blink.login_handler.send_auth_key, self.blink, pin + ): + return await self.async_step_user(user_input=self.data) + + return self.async_show_form( + step_id="2fa", + data_schema=vol.Schema( + {vol.Optional("pin"): vol.All(str, vol.Length(min=1))} + ), + ) + + async def async_step_import(self, import_data): + """Import blink config from configuration.yaml.""" + return await self.async_step_user(import_data) + + +class Require2FA(exceptions.HomeAssistantError): + """Error to indicate we require 2FA.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py new file mode 100644 index 00000000000..5ce22d10914 --- /dev/null +++ b/homeassistant/components/blink/const.py @@ -0,0 +1,25 @@ +"""Constants for Blink.""" +DOMAIN = "blink" +DEVICE_ID = "Home Assistant" + +CONF_CAMERA = "camera" +CONF_ALARM_CONTROL_PANEL = "alarm_control_panel" + +DEFAULT_BRAND = "Blink" +DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" +DEFAULT_SCAN_INTERVAL = 300 +DEFAULT_OFFSET = 1 +SIGNAL_UPDATE_BLINK = "blink_update" + +TYPE_CAMERA_ARMED = "motion_enabled" +TYPE_MOTION_DETECTED = "motion_detected" +TYPE_TEMPERATURE = "temperature" +TYPE_BATTERY = "battery" +TYPE_WIFI_STRENGTH = "wifi_strength" + +SERVICE_REFRESH = "blink_update" +SERVICE_TRIGGER = "trigger_camera" +SERVICE_SAVE_VIDEO = "save_video" +SERVICE_SEND_PIN = "send_pin" + +PLATFORMS = ("alarm_control_panel", "binary_sensor", "camera", "sensor") diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index d55510c44ad..ac42870fdb7 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,6 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.14.3"], - "codeowners": ["@fronzbot"] + "requirements": ["blinkpy==0.15.0"], + "codeowners": ["@fronzbot"], + "config_flow": true } diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 81616b463ec..eb9e309fc65 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -1,25 +1,29 @@ """Support for Blink system camera sensors.""" import logging -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.const import TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity -from . import BLINK_DATA, SENSORS +from .const import DOMAIN, TYPE_BATTERY, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH _LOGGER = logging.getLogger(__name__) +SENSORS = { + TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, "mdi:thermometer"], + TYPE_BATTERY: ["Battery", "", "mdi:battery-80"], + TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", "mdi:wifi-strength-2"], +} -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a Blink sensor.""" - if discovery_info is None: - return - data = hass.data[BLINK_DATA] - devs = [] + +async def async_setup_entry(hass, config, async_add_entities): + """Initialize a Blink sensor.""" + data = hass.data[DOMAIN][config.entry_id] + entities = [] for camera in data.cameras: - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: - devs.append(BlinkSensor(data, camera, sensor_type)) + for sensor_type in SENSORS: + entities.append(BlinkSensor(data, camera, sensor_type)) - add_entities(devs, True) + async_add_entities(entities) class BlinkSensor(Entity): @@ -28,7 +32,7 @@ class BlinkSensor(Entity): def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" name, units, icon = SENSORS[sensor_type] - self._name = f"{BLINK_DATA} {camera} {name}" + self._name = f"{DOMAIN} {camera} {name}" self._camera_name = name self._type = sensor_type self.data = data diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 37595837c11..9a4d00ee0b8 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -19,3 +19,10 @@ save_video: filename: description: Filename to writable path (directory may need to be included in whitelist_dirs in config) example: "/tmp/video.mp4" + +send_pin: + description: Send a new pin to blink for 2FA. + fields: + pin: + description: Pin received from blink. Leave empty if you only received a verification email. + example: "abc123" diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json new file mode 100644 index 00000000000..dcd4a488c5c --- /dev/null +++ b/homeassistant/components/blink/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "title": "Sign-in with Blink account", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "2fa": { + "title": "Two-factor authentication", + "data": { "2fa": "Two-factor code" }, + "description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank" + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/blink/translations/ca.json b/homeassistant/components/blink/translations/ca.json new file mode 100644 index 00000000000..68607b0dbcd --- /dev/null +++ b/homeassistant/components/blink/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/en.json b/homeassistant/components/blink/translations/en.json new file mode 100644 index 00000000000..2187e91f09e --- /dev/null +++ b/homeassistant/components/blink/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "2fa": { + "data": { + "2fa": "Two-factor code" + }, + "description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank", + "title": "Two-factor authentication" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Sign-in with Blink account" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/ru.json b/homeassistant/components/blink/translations/ru.json new file mode 100644 index 00000000000..70f828d2f7f --- /dev/null +++ b/homeassistant/components/blink/translations/ru.json @@ -0,0 +1,27 @@ +{ + "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": { + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u0412\u0430\u0448\u0443 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443. \u0415\u0441\u043b\u0438 \u043f\u0438\u0441\u044c\u043c\u043e \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 PIN-\u043a\u043e\u0434, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c", + "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json new file mode 100644 index 00000000000..f1e24cf7c7c --- /dev/null +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc" + }, + "description": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 PIN \u78bc\uff0c\u5047\u5982\u90f5\u4ef6\u4e2d\u6c92\u6709 PIN \u78bc\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d", + "title": "\u96d9\u91cd\u9a57\u8b49" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u4ee5 Blink \u5e33\u865f\u767b\u5165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 4eab2fc3d11..56009c90da3 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - 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([BlinkStickLight(stick, name)], True) -class BlinkStickLight(Light): +class BlinkStickLight(LightEntity): """Representation of a BlinkStick light.""" def __init__(self, stick, name): diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index d9ef2ac6a7e..768ca92d9d2 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class BlinktLight(Light): +class BlinktLight(LightEntity): """Representation of a Blinkt! Light.""" def __init__(self, blinkt, name, index): diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 4e6fc867f2d..077171006bf 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([BloomSkySensor(bloomsky, device, variable)], True) -class BloomSkySensor(BinarySensorDevice): +class BloomSkySensor(BinarySensorEntity): """Representation of a single binary sensor in a BloomSky device.""" def __init__(self, bs, device, sensor_name): diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index eb1ce5f30dc..c0088cb4a2a 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -12,7 +12,7 @@ import async_timeout import voluptuous as vol import xmltodict -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, @@ -200,7 +200,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class BluesoundPlayer(MediaPlayerDevice): +class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" def __init__(self, hass, host, port=None, name=None, init_callback=None): diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 4957356d26a..b2ea63abe69 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,9 +1,13 @@ """Tracking for bluetooth low energy devices.""" import asyncio +from datetime import datetime, timedelta import logging +from uuid import UUID import pygatt # pylint: disable=import-error +import voluptuous as vol +from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, @@ -15,16 +19,32 @@ from homeassistant.components.device_tracker.legacy import ( async_load_config, ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +# Base UUID: 00000000-0000-1000-8000-00805F9B34FB +# Battery characteristic: 0x2a19 (https://www.bluetooth.com/specifications/gatt/characteristics/) +BATTERY_CHARACTERISTIC_UUID = UUID("00002a19-0000-1000-8000-00805f9b34fb") +CONF_TRACK_BATTERY = "track_battery" +CONF_TRACK_BATTERY_INTERVAL = "track_battery_interval" +DEFAULT_TRACK_BATTERY_INTERVAL = timedelta(days=1) DATA_BLE = "BLE" DATA_BLE_ADAPTER = "ADAPTER" BLE_PREFIX = "BLE_" MIN_SEEN_NEW = 5 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_TRACK_BATTERY, default=False): cv.boolean, + vol.Optional( + CONF_TRACK_BATTERY_INTERVAL, default=DEFAULT_TRACK_BATTERY_INTERVAL + ): cv.time_period, + } +) + def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" @@ -42,7 +62,12 @@ def setup_scanner(hass, config, see, discovery_info=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) - def see_device(address, name, new_device=False): + if config[CONF_TRACK_BATTERY]: + battery_track_interval = config[CONF_TRACK_BATTERY_INTERVAL] + else: + battery_track_interval = timedelta(0) + + def see_device(address, name, new_device=False, battery=None): """Mark a device as seen.""" if name is not None: name = name.strip("\x00") @@ -59,6 +84,10 @@ def setup_scanner(hass, config, see, discovery_info=None): return _LOGGER.debug("Adding %s to tracked devices", address) devs_to_track.append(address) + if battery_track_interval > timedelta(0): + devs_track_battery[address] = dt_util.as_utc( + datetime.fromtimestamp(0) + ) else: _LOGGER.debug("Seen %s for the first time", address) new_devices[address] = {"seen": 1, "name": name} @@ -68,6 +97,7 @@ def setup_scanner(hass, config, see, discovery_info=None): mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, + battery=battery, ) def discover_ble_devices(): @@ -88,6 +118,7 @@ def setup_scanner(hass, config, see, discovery_info=None): yaml_path = hass.config.path(YAML_DEVICES) devs_to_track = [] devs_donot_track = [] + devs_track_battery = {} # Load all known devices. # We just need the devices so set consider_home and home range @@ -97,12 +128,17 @@ def setup_scanner(hass, config, see, discovery_info=None): ).result(): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: + address = device.mac[4:] if device.track: _LOGGER.debug("Adding %s to BLE tracker", device.mac) - devs_to_track.append(device.mac[4:]) + devs_to_track.append(address) + if battery_track_interval > timedelta(0): + devs_track_battery[address] = dt_util.as_utc( + datetime.fromtimestamp(0) + ) else: _LOGGER.debug("Adding %s to BLE do not track", device.mac) - devs_donot_track.append(device.mac[4:]) + devs_donot_track.append(address) # if track new devices is true discover new devices # on every scan. @@ -117,13 +153,41 @@ def setup_scanner(hass, config, see, discovery_info=None): def update_ble(now): """Lookup Bluetooth LE devices and update status.""" devs = discover_ble_devices() + if devs_track_battery: + adapter = hass.data[DATA_BLE][DATA_BLE_ADAPTER] for mac in devs_to_track: if mac not in devs: continue if devs[mac] is None: devs[mac] = mac - see_device(mac, devs[mac]) + + battery = None + if ( + mac in devs_track_battery + and now > devs_track_battery[mac] + battery_track_interval + ): + handle = None + try: + adapter.start(reset_on_start=True) + _LOGGER.debug("Reading battery for Bluetooth LE device %s", mac) + bt_device = adapter.connect(mac) + # Try to get the handle; it will raise a BLEError exception if not available + handle = bt_device.get_handle(BATTERY_CHARACTERISTIC_UUID) + battery = ord(bt_device.char_read(BATTERY_CHARACTERISTIC_UUID)) + devs_track_battery[mac] = now + except pygatt.exceptions.NotificationTimeout: + _LOGGER.warning("Timeout when trying to get battery status") + except pygatt.exceptions.BLEError as err: + _LOGGER.warning("Could not read battery status: %s", err) + if handle is not None: + # If the device does not offer battery information, there is no point in asking again later on. + # Remove the device from the battery-tracked devices, so that their battery is not wasted + # trying to get an unavailable information. + del devs_track_battery[mac] + finally: + adapter.stop() + see_device(mac, devs[mac], battery=battery) if track_new: for address in devs: diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index fc3069f284c..ee89873e8fe 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -3,7 +3,7 @@ import logging from bimmer_connected.state import ChargingState, LockState -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS from . import DOMAIN as BMW_DOMAIN @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class BMWConnectedDriveSensor(BinarySensorDevice): +class BMWConnectedDriveSensor(BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" def __init__( diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 7d4ad420af4..d30f1702ae8 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -3,7 +3,7 @@ import logging from bimmer_connected.state import LockState -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_ATTRIBUTION, STATE_LOCKED, STATE_UNLOCKED from . import DOMAIN as BMW_DOMAIN @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class BMWLock(LockDevice): +class BMWLock(LockEntity): """Representation of a BMW vehicle lock.""" def __init__(self, account, vehicle, attribute: str, sensor_name): diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 98c1dca08d2..0936c1f9088 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["bravia-tv==1.0.3"], + "requirements": ["bravia-tv==1.0.4"], "codeowners": ["@robbiet480", "@bieniu"], "config_flow": true } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index e4b28c0c2ab..eb75542460f 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( DEVICE_CLASS_TV, PLATFORM_SCHEMA, - MediaPlayerDevice, + MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -117,7 +117,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class BraviaTVDevice(MediaPlayerDevice): +class BraviaTVDevice(MediaPlayerEntity): """Representation of a Bravia TV.""" def __init__(self, client, name, pin, unique_id, device_info, ignored_sources): diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index 1e434cd118a..ca432270cbb 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -4,12 +4,16 @@ "user": { "title": "Sony Bravia TV", "description": "Set up Sony Bravia TV integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/braviatv \n\nEnsure that your TV is turned on.", - "data": { "host": "TV hostname or IP address" } + "data": { + "host": "[%key:common::config_flow::data::host%]" + } }, "authorize": { "title": "Authorize Sony Bravia TV", "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Unregister remote device.", - "data": { "pin": "PIN code" } + "data": { + "pin": "PIN code" + } } }, "error": { @@ -17,14 +21,18 @@ "cannot_connect": "Failed to connect, invalid host or PIN code.", "unsupported_model": "Your TV model is not supported." }, - "abort": { "already_configured": "This TV is already configured." } + "abort": { + "already_configured": "This TV is already configured." + } }, "options": { "step": { "user": { "title": "Options for Sony Bravia TV", - "data": { "ignored_sources": "List of ignored sources" } + "data": { + "ignored_sources": "List of ignored sources" + } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index d92525ae325..5a6d50c5c53 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Nom d\u2019amfitri\u00f3 o adre\u00e7a IP del televisor" + "host": "Nom d'amfitri\u00f3 o adre\u00e7a IP del televisor" }, "description": "Configura la integraci\u00f3 de televisor Sony Bravia. Si tens problemes durant la configuraci\u00f3, v\u00e9s a: https://www.home-assistant.io/integrations/braviatv\n\nAssegura't que el televisor estigui engegat.", "title": "Televisor Sony Bravia" diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 04ab5830739..c9bc5a4469b 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "TV hostname or IP address" + "host": "Host" }, "description": "Set up Sony Bravia TV integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/braviatv \n\nEnsure that your TV is turned on.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/braviatv/translations/es-419.json b/homeassistant/components/braviatv/translations/es-419.json new file mode 100644 index 00000000000..820ea329a0c --- /dev/null +++ b/homeassistant/components/braviatv/translations/es-419.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Esta televisi\u00f3n ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se pudo conectar, host inv\u00e1lido o c\u00f3digo PIN.", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos.", + "unsupported_model": "Su modelo de televisi\u00f3n no es compatible." + }, + "step": { + "authorize": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "Ingrese el c\u00f3digo PIN que se muestra en la televisi\u00f3n Sony Bravia. \n\nSi no se muestra el c\u00f3digo PIN, debe cancelar el registro de Home Assistant en su televisi\u00f3n, vaya a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto - > Cancelar registro del dispositivo remoto.", + "title": "Autorizar Sony Bravia TV" + }, + "user": { + "data": { + "host": "Nombre de host de TV o direcci\u00f3n IP" + }, + "description": "Configure la integraci\u00f3n de Sony Bravia TV. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/braviatv \n\n Aseg\u00farese de que su televisi\u00f3n est\u00e9 encendida.", + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lista de fuentes ignoradas" + }, + "title": "Opciones para Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 3652210f7b7..593fd997709 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "TV \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c" + "host": "\ud638\uc2a4\ud2b8" }, "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV \uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json new file mode 100644 index 00000000000..ba09fbca3a3 --- /dev/null +++ b/homeassistant/components/braviatv/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Deze tv is al geconfigureerd." + }, + "error": { + "cannot_connect": "Geen verbinding, ongeldige host of PIN-code.", + "invalid_host": "Ongeldige hostnaam of IP-adres.", + "unsupported_model": "Uw tv-model wordt niet ondersteund." + }, + "step": { + "authorize": { + "data": { + "pin": "PIN-code" + }, + "description": "Voer de pincode in die wordt weergegeven op de Sony Bravia tv. \n\nAls de pincode niet wordt weergegeven, moet u de Home Assistant op uw tv afmelden, ga naar: Instellingen -> Netwerk -> Instellingen extern apparaat -> Afmelden extern apparaat.", + "title": "Autoriseer Sony Bravia tv" + }, + "user": { + "data": { + "host": "Hostnaam of IP-adres van tv" + }, + "description": "Stel Sony Bravia TV-integratie in. Als je problemen hebt met de configuratie ga dan naar: https://www.home-assistant.io/integrations/braviatv \n\nZorg ervoor dat uw tv is ingeschakeld.", + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lijst met genegeerde bronnen" + }, + "title": "Opties voor Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index afd0f0d758c..f6100b7843a 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -13,15 +13,15 @@ "data": { "pin": "PIN-kode" }, - "description": "Tast inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrere ekstern enhet.", - "title": "Autoriser Sony Bravia TV" + "description": "Angi PIN-koden som vises p\u00e5 Sony Bravia TV. \n\nHvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en, g\u00e5 til: Innstillinger -> Nettverk -> Innstillinger for ekstern enhet -> Avregistrere ekstern enhet.", + "title": "Godkjenn Sony Bravia TV" }, "user": { "data": { "host": "TV-vertsnavn eller IP-adresse" }, - "description": "Konfigurer Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: https://www.home-assistant.io/integrations/braviatv \n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.", - "title": "Sony Bravia TV" + "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.", + "title": "" } } }, diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index cbafa3c4b44..e20679c4172 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ten telewizor jest ju\u017c skonfigurowany." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Po\u0142\u0105czenie nieudane, nieprawid\u0142owy host lub kod PIN.", @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP telewizora" + "host": "[%key_id:common::config_flow::data::host%]" }, "description": "Konfiguracja integracji telewizora Sony Bravia. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a do strony: https://www.home-assistant.io/integrations/braviatv\n\nUpewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 33e8d71bd64..e1cabf3bf4e 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -18,9 +18,9 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:\nhttps://www.home-assistant.io/integrations/braviatv", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:\nhttps://www.home-assistant.io/integrations/braviatv", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Sony Bravia" } } diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json new file mode 100644 index 00000000000..6ec160e799a --- /dev/null +++ b/homeassistant/components/braviatv/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad" + }, + "error": { + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress.", + "unsupported_model": "Den h\u00e4r tv modellen st\u00f6ds inte." + }, + "step": { + "authorize": { + "data": { + "pin": "Pin-kod" + }, + "title": "Auktorisera Sony Bravia TV" + }, + "user": { + "data": { + "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" + }, + "title": "Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index cf2c87f4c93..5841c550330 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "\u96fb\u8996\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740" + "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u8a2d\u5b9a Sony Bravia \u96fb\u8996\u6574\u5408\u3002\u5047\u5982\u65bc\u8a2d\u5b9a\u904e\u7a0b\u4e2d\u906d\u9047\u56f0\u7136\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/braviatv \n\n\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u958b\u555f\u3002", "title": "Sony Bravia \u96fb\u8996" diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index be6aa266491..040b22945fd 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -5,12 +5,11 @@ from binascii import unhexlify from datetime import timedelta import logging import re -import socket +from broadlink.exceptions import BroadlinkException, ReadError import voluptuous as vol from homeassistant.const import CONF_HOST -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -65,36 +64,35 @@ SERVICE_SEND_SCHEMA = vol.Schema( SERVICE_LEARN_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) -@callback -def async_setup_service(hass, host, device): +async def async_setup_service(hass, host, device): """Register a device for given host for use in services.""" hass.data.setdefault(DOMAIN, {})[host] = device if hass.services.has_service(DOMAIN, SERVICE_LEARN): return - async def _learn_command(call): + async def async_learn_command(call): """Learn a packet from remote.""" device = hass.data[DOMAIN][call.data[CONF_HOST]] - for retry in range(DEFAULT_RETRY): - try: - await hass.async_add_executor_job(device.enter_learning) - break - except (socket.timeout, ValueError): - try: - await hass.async_add_executor_job(device.auth) - except socket.timeout: - if retry == DEFAULT_RETRY - 1: - _LOGGER.error("Failed to enter learning mode") - return + try: + await device.async_request(device.api.enter_learning) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to enter learning mode: %s", err_msg) + return _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=20): - packet = await hass.async_add_executor_job(device.check_data) - if packet: + try: + packet = await device.async_request(device.api.check_data) + except ReadError: + await asyncio.sleep(1) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to learn: %s", err_msg) + return + else: data = b64encode(packet).decode("utf8") log_msg = f"Received packet is: {data}" _LOGGER.info(log_msg) @@ -102,32 +100,26 @@ def async_setup_service(hass, host, device): log_msg, title="Broadlink switch" ) return - await asyncio.sleep(1) - _LOGGER.error("No signal was received") + _LOGGER.error("Failed to learn: No signal received") hass.components.persistent_notification.async_create( "No signal was received", title="Broadlink switch" ) hass.services.async_register( - DOMAIN, SERVICE_LEARN, _learn_command, schema=SERVICE_LEARN_SCHEMA + DOMAIN, SERVICE_LEARN, async_learn_command, schema=SERVICE_LEARN_SCHEMA ) - async def _send_packet(call): + async def async_send_packet(call): """Send a packet.""" device = hass.data[DOMAIN][call.data[CONF_HOST]] packets = call.data[CONF_PACKET] for packet in packets: - for retry in range(DEFAULT_RETRY): - try: - await hass.async_add_executor_job(device.send_data, packet) - break - except (socket.timeout, ValueError): - try: - await hass.async_add_executor_job(device.auth) - except socket.timeout: - if retry == DEFAULT_RETRY - 1: - _LOGGER.error("Failed to send packet to device") + try: + await device.async_request(device.api.send_data, packet) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return hass.services.async_register( - DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA + DOMAIN, SERVICE_SEND, async_send_packet, schema=SERVICE_SEND_SCHEMA ) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py new file mode 100644 index 00000000000..a036557e58a --- /dev/null +++ b/homeassistant/components/broadlink/device.py @@ -0,0 +1,57 @@ +"""Support for Broadlink devices.""" +from functools import partial +import logging + +from broadlink.exceptions import ( + AuthorizationError, + BroadlinkException, + ConnectionClosedError, + DeviceOfflineError, +) + +from .const import DEFAULT_RETRY + +_LOGGER = logging.getLogger(__name__) + + +class BroadlinkDevice: + """Manages a Broadlink device.""" + + def __init__(self, hass, api): + """Initialize the device.""" + self.hass = hass + self.api = api + self.available = None + + async def async_connect(self): + """Connect to the device.""" + try: + await self.hass.async_add_executor_job(self.api.auth) + except BroadlinkException as err_msg: + if self.available: + self.available = False + _LOGGER.warning( + "Disconnected from device at %s: %s", self.api.host[0], err_msg + ) + return False + else: + if not self.available: + if self.available is not None: + _LOGGER.warning("Connected to device at %s", self.api.host[0]) + self.available = True + return True + + async def async_request(self, function, *args, **kwargs): + """Send a request to the device.""" + partial_function = partial(function, *args, **kwargs) + for attempt in range(DEFAULT_RETRY): + try: + result = await self.hass.async_add_executor_job(partial_function) + except (AuthorizationError, ConnectionClosedError, DeviceOfflineError): + if attempt == DEFAULT_RETRY - 1 or not await self.async_connect(): + raise + else: + if not self.available: + self.available = True + _LOGGER.warning("Connected to device at %s", self.api.host[0]) + return result diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index f894fe46a53..76443ae7467 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,6 +2,6 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.13.2"], + "requirements": ["broadlink==0.14.0"], "codeowners": ["@danielhiversen", "@felipediel"] } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 177cf2ee0bd..b03bf7a4a04 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -9,6 +9,12 @@ from itertools import product import logging import broadlink as blk +from broadlink.exceptions import ( + AuthorizationError, + BroadlinkException, + DeviceOfflineError, + ReadError, +) import voluptuous as vol from homeassistant.components.remote import ( @@ -22,7 +28,7 @@ from homeassistant.components.remote import ( DOMAIN as COMPONENT, PLATFORM_SCHEMA, SUPPORT_LEARN_COMMAND, - RemoteDevice, + RemoteEntity, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.core import callback @@ -36,11 +42,11 @@ from .const import ( DEFAULT_LEARNING_TIMEOUT, DEFAULT_NAME, DEFAULT_PORT, - DEFAULT_RETRY, DEFAULT_TIMEOUT, RM4_TYPES, RM_TYPES, ) +from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) @@ -103,17 +109,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= else: api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) api.timeout = timeout + device = BroadlinkDevice(hass, api) + code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes") flag_storage = Store(hass, FLAG_STORAGE_VERSION, f"broadlink_{unique_id}_flags") - remote = BroadlinkRemote(name, unique_id, api, code_storage, flag_storage) - connected, loaded = (False, False) - try: - connected, loaded = await asyncio.gather( - hass.async_add_executor_job(api.auth), remote.async_load_storage_files() - ) - except OSError: - pass + remote = BroadlinkRemote(name, unique_id, device, code_storage, flag_storage) + + connected, loaded = await asyncio.gather( + device.async_connect(), remote.async_load_storage_files() + ) if not connected: hass.data[DOMAIN][COMPONENT].remove(unique_id) raise PlatformNotReady @@ -124,14 +129,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([remote], False) -class BroadlinkRemote(RemoteDevice): +class BroadlinkRemote(RemoteEntity): """Representation of a Broadlink remote.""" - def __init__(self, name, unique_id, api, code_storage, flag_storage): + def __init__(self, name, unique_id, device, code_storage, flag_storage): """Initialize the remote.""" + self.device = device self._name = name self._unique_id = unique_id - self._api = api self._code_storage = code_storage self._flag_storage = flag_storage self._codes = {} @@ -157,7 +162,7 @@ class BroadlinkRemote(RemoteDevice): @property def available(self): """Return True if the remote is available.""" - return self._available + return self.device.available @property def supported_features(self): @@ -182,9 +187,9 @@ class BroadlinkRemote(RemoteDevice): self._state = False async def async_update(self): - """Update the availability of the remote.""" + """Update the availability of the device.""" if not self.available: - await self._async_connect() + await self.device.async_connect() async def async_load_storage_files(self): """Load codes and toggle flags from storage files.""" @@ -213,8 +218,10 @@ class BroadlinkRemote(RemoteDevice): should_delay = await self._async_send_code( cmd, device, delay if should_delay else 0 ) - except ConnectionError: + except (AuthorizationError, DeviceOfflineError): break + except BroadlinkException: + pass self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) @@ -227,7 +234,7 @@ class BroadlinkRemote(RemoteDevice): try: code = self._codes[device][command] except KeyError: - _LOGGER.error("Failed to send '%s/%s': command not found", command, device) + _LOGGER.error("Failed to send '%s/%s': Command not found", command, device) return False if isinstance(code, list): @@ -238,12 +245,14 @@ class BroadlinkRemote(RemoteDevice): await asyncio.sleep(delay) try: - await self._async_attempt(self._api.send_data, data_packet(code)) + await self.device.async_request( + self.device.api.send_data, data_packet(code) + ) except ValueError: - _LOGGER.error("Failed to send '%s/%s': invalid code", command, device) + _LOGGER.error("Failed to send '%s/%s': Invalid code", command, device) return False - except ConnectionError: - _LOGGER.error("Failed to send '%s/%s': remote is offline", command, device) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send '%s/%s': %s", command, device, err_msg) raise if should_alternate: @@ -268,8 +277,10 @@ class BroadlinkRemote(RemoteDevice): should_store |= await self._async_learn_code( command, device, toggle, timeout ) - except ConnectionError: + except (AuthorizationError, DeviceOfflineError): break + except BroadlinkException: + pass if should_store: await self._code_storage.async_save(self._codes) @@ -287,22 +298,19 @@ class BroadlinkRemote(RemoteDevice): await self._async_capture_code(command, timeout), await self._async_capture_code(command, timeout), ] - except (ValueError, TimeoutError): - _LOGGER.error( - "Failed to learn '%s/%s': no signal received", command, device - ) + except TimeoutError: + _LOGGER.error("Failed to learn '%s/%s': No code received", command, device) return False - except ConnectionError: - _LOGGER.error("Failed to learn '%s/%s': remote is offline", command, device) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to learn '%s/%s': %s", command, device, err_msg) raise self._codes.setdefault(device, {}).update({command: code}) - return True async def _async_capture_code(self, command, timeout): """Enter learning mode and capture a code from a remote.""" - await self._async_attempt(self._api.enter_learning) + await self.device.async_request(self.device.api.enter_learning) self.hass.components.persistent_notification.async_create( f"Press the '{command}' button.", @@ -313,44 +321,18 @@ class BroadlinkRemote(RemoteDevice): code = None start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=timeout): - code = await self.hass.async_add_executor_job(self._api.check_data) - if code: + try: + code = await self.device.async_request(self.device.api.check_data) + except ReadError: + await asyncio.sleep(1) + else: break - await asyncio.sleep(1) self.hass.components.persistent_notification.async_dismiss( notification_id="learn_command" ) - if not code: + if code is None: raise TimeoutError - if all(not value for value in code): - raise ValueError return b64encode(code).decode("utf8") - - async def _async_attempt(self, function, *args): - """Retry a socket-related function until it succeeds.""" - for retry in range(DEFAULT_RETRY): - if retry and not await self._async_connect(): - continue - try: - await self.hass.async_add_executor_job(function, *args) - except OSError: - continue - return - raise ConnectionError - - async def _async_connect(self): - """Connect to the remote.""" - try: - auth = await self.hass.async_add_executor_job(self._api.auth) - except OSError: - auth = False - if auth and not self._available: - _LOGGER.warning("Connected to the remote") - self._available = True - elif not auth and self._available: - _LOGGER.warning("Disconnected from the remote") - self._available = False - return auth diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 13b1530eb6d..da2bc534859 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -4,6 +4,7 @@ from ipaddress import ip_address import logging import broadlink as blk +from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -18,6 +19,7 @@ from homeassistant.const import ( TEMP_CELSIUS, UNIT_PERCENTAGE, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -27,10 +29,12 @@ from .const import ( A1_TYPES, DEFAULT_NAME, DEFAULT_PORT, + DEFAULT_RETRY, DEFAULT_TIMEOUT, RM4_TYPES, RM_TYPES, ) +from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) @@ -60,7 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Broadlink device sensors.""" host = config[CONF_HOST] mac_addr = config[CONF_MAC] @@ -77,11 +81,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): check_sensors = api.check_sensors_raw api.timeout = timeout - broadlink_data = BroadlinkData(api, check_sensors, update_interval) - dev = [] - for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append(BroadlinkSensor(name, broadlink_data, variable)) - add_entities(dev, True) + device = BroadlinkDevice(hass, api) + + connected = await device.async_connect() + if not connected: + raise PlatformNotReady + + broadlink_data = BroadlinkData(device, check_sensors, update_interval) + sensors = [ + BroadlinkSensor(name, broadlink_data, variable) + for variable in config[CONF_MONITORED_CONDITIONS] + ] + async_add_entities(sensors, True) class BroadlinkSensor(Entity): @@ -91,7 +102,6 @@ class BroadlinkSensor(Entity): """Initialize the sensor.""" self._name = f"{name} {SENSOR_TYPES[sensor_type][0]}" self._state = None - self._is_available = False self._type = sensor_type self._broadlink_data = broadlink_data self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -109,32 +119,27 @@ class BroadlinkSensor(Entity): @property def available(self): """Return True if entity is available.""" - return self._is_available + return self._broadlink_data.device.available @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement - def update(self): + async def async_update(self): """Get the latest data from the sensor.""" - self._broadlink_data.update() - if self._broadlink_data.data is None: - self._state = None - self._is_available = False - return - self._state = self._broadlink_data.data[self._type] - self._is_available = True + await self._broadlink_data.async_update() + self._state = self._broadlink_data.data.get(self._type) class BroadlinkData: """Representation of a Broadlink data object.""" - def __init__(self, api, check_sensors, interval): + def __init__(self, device, check_sensors, interval): """Initialize the data object.""" - self.api = api + self.device = device self.check_sensors = check_sensors - self.data = None + self.data = {} self._schema = vol.Schema( { vol.Optional("temperature"): vol.Range(min=-50, max=150), @@ -144,31 +149,21 @@ class BroadlinkData: vol.Optional("noise"): vol.Any(0, 1, 2), } ) - self.update = Throttle(interval)(self._update) - if not self._auth(): - _LOGGER.warning("Failed to connect to device") + self.async_update = Throttle(interval)(self._async_fetch_data) - def _update(self, retry=3): - try: - data = self.check_sensors() - if data is not None: - self.data = self._schema(data) + async def _async_fetch_data(self): + """Fetch sensor data.""" + for _ in range(DEFAULT_RETRY): + try: + data = await self.device.async_request(self.check_sensors) + except BroadlinkException: return - except OSError as error: - if retry < 1: - self.data = None - _LOGGER.error(error) + try: + data = self._schema(data) + except (vol.Invalid, vol.MultipleInvalid): + continue + else: + self.data = data return - except (vol.Invalid, vol.MultipleInvalid): - pass # Continue quietly if device returned malformed data - if retry > 0 and self._auth(): - self._update(retry - 1) - def _auth(self, retry=3): - try: - auth = self.api.auth() - except OSError: - auth = False - if not auth and retry > 0: - return self._auth(retry - 1) - return auth + _LOGGER.debug("Failed to update sensors: Device returned malformed data") diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 6eed0646d76..b62da4ebb3e 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -2,12 +2,12 @@ from datetime import timedelta from ipaddress import ip_address import logging -import socket import broadlink as blk +from broadlink.exceptions import BroadlinkException import voluptuous as vol -from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_TYPE, STATE_ON, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import Throttle, slugify @@ -27,7 +28,6 @@ from . import async_setup_service, data_packet, hostname, mac_address from .const import ( DEFAULT_NAME, DEFAULT_PORT, - DEFAULT_RETRY, DEFAULT_TIMEOUT, MP1_TYPES, RM4_TYPES, @@ -35,6 +35,7 @@ from .const import ( SP1_TYPES, SP2_TYPES, ) +from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) @@ -73,21 +74,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TYPE, default=DEVICE_TYPES[0]): vol.In(DEVICE_TYPES), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Broadlink switches.""" - devices = config.get(CONF_SWITCHES) - slots = config.get("slots", {}) - host = config.get(CONF_HOST) - mac_addr = config.get(CONF_MAC) - friendly_name = config.get(CONF_FRIENDLY_NAME) + host = config[CONF_HOST] + mac_addr = config[CONF_MAC] + friendly_name = config[CONF_FRIENDLY_NAME] model = config[CONF_TYPE] - retry_times = config.get(CONF_RETRY) + timeout = config[CONF_TIMEOUT] + slots = config[CONF_SLOTS] + devices = config[CONF_SWITCHES] def generate_rm_switches(switches, broadlink_device): """Generate RM switches.""" @@ -98,7 +98,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): broadlink_device, config.get(CONF_COMMAND_ON), config.get(CONF_COMMAND_OFF), - retry_times, ) for object_id, config in switches.items() ] @@ -110,58 +109,54 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return slots[f"slot_{slot}"] if model in RM_TYPES: - broadlink_device = blk.rm((host, DEFAULT_PORT), mac_addr, None) - hass.add_job(async_setup_service, hass, host, broadlink_device) + api = blk.rm((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) switches = generate_rm_switches(devices, broadlink_device) elif model in RM4_TYPES: - broadlink_device = blk.rm4((host, DEFAULT_PORT), mac_addr, None) - hass.add_job(async_setup_service, hass, host, broadlink_device) + api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) switches = generate_rm_switches(devices, broadlink_device) elif model in SP1_TYPES: - broadlink_device = blk.sp1((host, DEFAULT_PORT), mac_addr, None) - switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] + api = blk.sp1((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) + switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] elif model in SP2_TYPES: - broadlink_device = blk.sp2((host, DEFAULT_PORT), mac_addr, None) - switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] + api = blk.sp2((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) + switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] elif model in MP1_TYPES: - switches = [] - broadlink_device = blk.mp1((host, DEFAULT_PORT), mac_addr, None) - parent_device = BroadlinkMP1Switch(broadlink_device, retry_times) - for i in range(1, 5): - slot = BroadlinkMP1Slot( - get_mp1_slot_name(friendly_name, i), - broadlink_device, - i, - parent_device, - retry_times, + api = blk.mp1((host, DEFAULT_PORT), mac_addr, None) + broadlink_device = BroadlinkDevice(hass, api) + parent_device = BroadlinkMP1Switch(broadlink_device) + switches = [ + BroadlinkMP1Slot( + get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device, ) - switches.append(slot) + for i in range(1, 5) + ] - broadlink_device.timeout = config.get(CONF_TIMEOUT) - try: - broadlink_device.auth() - except OSError: - _LOGGER.error("Failed to connect to device") + api.timeout = timeout + connected = await broadlink_device.async_connect() + if not connected: + raise PlatformNotReady - add_entities(switches) + if model in RM_TYPES or model in RM4_TYPES: + hass.async_create_task(async_setup_service(hass, host, broadlink_device)) + + async_add_entities(switches) -class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): +class BroadlinkRMSwitch(SwitchEntity, RestoreEntity): """Representation of an Broadlink switch.""" - def __init__( - self, name, friendly_name, device, command_on, command_off, retry_times - ): + def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" + self.device = device self.entity_id = f"{DOMAIN}.{slugify(name)}" self._name = friendly_name self._state = False self._command_on = command_on self._command_off = command_off - self._device = device - self._is_available = False - self._retry_times = retry_times - _LOGGER.debug("_retry_times : %s", self._retry_times) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -183,7 +178,7 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): @property def available(self): """Return True if entity is available.""" - return not self.should_poll or self._is_available + return not self.should_poll or self.device.available @property def should_poll(self): @@ -195,68 +190,53 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + async def async_update(self): + """Update the state of the device.""" + if not self.available: + await self.device.async_connect() + + async def async_turn_on(self, **kwargs): """Turn the device on.""" - if self._sendpacket(self._command_on, self._retry_times): + if await self._async_send_packet(self._command_on): self._state = True - self.schedule_update_ha_state() + self.async_write_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" - if self._sendpacket(self._command_off, self._retry_times): + if await self._async_send_packet(self._command_off): self._state = False - self.schedule_update_ha_state() + self.async_write_ha_state() - def _sendpacket(self, packet, retry): + async def _async_send_packet(self, packet): """Send packet to device.""" if packet is None: _LOGGER.debug("Empty packet") return True try: - self._device.send_data(packet) - except (ValueError, OSError) as error: - if retry < 1: - _LOGGER.error("Error during sending a packet: %s", error) - return False - if not self._auth(self._retry_times): - return False - return self._sendpacket(packet, retry - 1) + await self.device.async_request(self.device.api.send_data, packet) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return False return True - def _auth(self, retry): - _LOGGER.debug("_auth : retry=%s", retry) - try: - auth = self._device.auth() - except OSError: - auth = False - if retry < 1: - _LOGGER.error("Timeout during authorization") - if not auth and retry > 0: - return self._auth(retry - 1) - return auth - class BroadlinkSP1Switch(BroadlinkRMSwitch): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device, retry_times): + def __init__(self, friendly_name, device): """Initialize the switch.""" - super().__init__(friendly_name, friendly_name, device, None, None, retry_times) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._load_power = None - def _sendpacket(self, packet, retry): + async def _async_send_packet(self, packet): """Send packet to device.""" try: - self._device.set_power(packet) - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during sending a packet: %s", error) - return False - if not self._auth(self._retry_times): - return False - return self._sendpacket(packet, retry - 1) + await self.device.async_request(self.device.api.set_power, packet) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return False return True @@ -281,37 +261,24 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): except (ValueError, TypeError): return None - def update(self): - """Synchronize state with switch.""" - self._update(self._retry_times) - - def _update(self, retry): + async def async_update(self): """Update the state of the device.""" - _LOGGER.debug("_update : retry=%s", retry) try: - state = self._device.check_power() - load_power = self._device.get_energy() - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during updating the state: %s", error) - self._is_available = False - return - if not self._auth(self._retry_times): - return - return self._update(retry - 1) - if state is None and retry > 0: - return self._update(retry - 1) + state = await self.device.async_request(self.device.api.check_power) + load_power = await self.device.async_request(self.device.api.get_energy) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to update state: %s", err_msg) + return self._state = state self._load_power = load_power - self._is_available = True class BroadlinkMP1Slot(BroadlinkRMSwitch): """Representation of a slot of Broadlink switch.""" - def __init__(self, friendly_name, device, slot, parent_device, retry_times): + def __init__(self, friendly_name, device, slot, parent_device): """Initialize the slot of switch.""" - super().__init__(friendly_name, friendly_name, device, None, None, retry_times) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._slot = slot @@ -322,44 +289,35 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): """Return true if unable to access real state of entity.""" return False - def _sendpacket(self, packet, retry): - """Send packet to device.""" - try: - self._device.set_power(self._slot, packet) - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during sending a packet: %s", error) - self._is_available = False - return False - if not self._auth(self._retry_times): - return False - return self._sendpacket(packet, max(0, retry - 1)) - self._is_available = True - return True - @property def should_poll(self): """Return the polling state.""" return True - def update(self): - """Trigger update for all switches on the parent device.""" - self._parent_device.update() + async def async_update(self): + """Update the state of the device.""" + await self._parent_device.async_update() self._state = self._parent_device.get_outlet_status(self._slot) - if self._state is None: - self._is_available = False - else: - self._is_available = True + + async def _async_send_packet(self, packet): + """Send packet to device.""" + try: + await self.device.async_request( + self.device.api.set_power, self._slot, packet + ) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to send packet: %s", err_msg) + return False + return True class BroadlinkMP1Switch: """Representation of a Broadlink switch - To fetch states of all slots.""" - def __init__(self, device, retry_times): + def __init__(self, device): """Initialize the switch.""" - self._device = device + self.device = device self._states = None - self._retry_times = retry_times def get_outlet_status(self, slot): """Get status of outlet from cached status list.""" @@ -368,31 +326,10 @@ class BroadlinkMP1Switch: return self._states[f"s{slot}"] @Throttle(TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for this device.""" - self._update(self._retry_times) - - def _update(self, retry): + async def async_update(self): """Update the state of the device.""" try: - states = self._device.check_power() - except (socket.timeout, ValueError) as error: - if retry < 1: - _LOGGER.error("Error during updating the state: %s", error) - return - if not self._auth(self._retry_times): - return - return self._update(max(0, retry - 1)) - if states is None and retry > 0: - return self._update(max(0, retry - 1)) + states = await self.device.async_request(self.device.api.check_power) + except BroadlinkException as err_msg: + _LOGGER.error("Failed to update state: %s", err_msg) self._states = states - - def _auth(self, retry): - """Authenticate the device.""" - try: - auth = self._device.auth() - except OSError: - auth = False - if not auth and retry > 0: - return self._auth(retry - 1) - return auth diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 76e62731e53..264992a7eae 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -5,14 +5,16 @@ "user": { "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", "data": { - "host": "Printer hostname or IP address", + "host": "[%key:common::config_flow::data::host%]", "type": "Type of the printer" } }, "zeroconf_confirm": { "description": "Do you want to add the Brother Printer {model} with serial number `{serial_number}` to Home Assistant?", "title": "Discovered Brother Printer", - "data": { "type": "Type of the printer" } + "data": { + "type": "Type of the printer" + } } }, "error": { @@ -25,4 +27,4 @@ "already_configured": "This printer is already configured." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/en.json b/homeassistant/components/brother/translations/en.json index 6d453478083..78fdc344d65 100644 --- a/homeassistant/components/brother/translations/en.json +++ b/homeassistant/components/brother/translations/en.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Printer hostname or IP address", + "host": "Host", "type": "Type of the printer" }, "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", diff --git a/homeassistant/components/brother/translations/es-419.json b/homeassistant/components/brother/translations/es-419.json index 337f5b624fb..286851ba454 100644 --- a/homeassistant/components/brother/translations/es-419.json +++ b/homeassistant/components/brother/translations/es-419.json @@ -6,7 +6,26 @@ }, "error": { "connection_error": "Error de conexi\u00f3n.", - "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible." + "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.", + "wrong_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos." + }, + "flow_title": "Impresora Brother: {model} {serial_number}", + "step": { + "user": { + "data": { + "host": "Nombre de host de la impresora o direcci\u00f3n IP", + "type": "Tipo de impresora" + }, + "description": "Configure la integraci\u00f3n de la impresora Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother", + "title": "Impresora Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo de impresora" + }, + "description": "\u00bfDesea agregar la Impresora Brother {model} con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "title": "Impresora Brother descubierta" + } } } } \ No newline at end of file diff --git a/homeassistant/components/brother/translations/ko.json b/homeassistant/components/brother/translations/ko.json index 5b79c87c175..9c8c2cfb165 100644 --- a/homeassistant/components/brother/translations/ko.json +++ b/homeassistant/components/brother/translations/ko.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "\ud504\ub9b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "host": "\ud638\uc2a4\ud2b8", "type": "\ud504\ub9b0\ud130\uc758 \uc885\ub958" }, "description": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00\uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/brother \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", diff --git a/homeassistant/components/brother/translations/no.json b/homeassistant/components/brother/translations/no.json index c612e5f8986..26f7d0a79e7 100644 --- a/homeassistant/components/brother/translations/no.json +++ b/homeassistant/components/brother/translations/no.json @@ -16,15 +16,15 @@ "host": "Vertsnavn eller IP-adresse til skriveren", "type": "Skriver type" }, - "description": "Konfigurer Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: https://www.home-assistant.io/integrations/brother", + "description": "Sett opp Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: [https://www.home-assistant.io/integrations/brother](https://www.home-assistant.io/integrations/brother)", "title": "Brother skriver" }, "zeroconf_confirm": { "data": { "type": "Type skriver" }, - "description": "Vil du legge til Brother-skriveren {Model} med serienummeret {serial_number} til Home Assistant?", - "title": "Oppdaget Brother-Skriveren" + "description": "Vil du legge til Brother-skriveren {model} med serienummeret `{serial_number}` til Home Assistant?", + "title": "Oppdaget Brother Skriver" } } } diff --git a/homeassistant/components/brother/translations/pl.json b/homeassistant/components/brother/translations/pl.json index 94f23b8b5d2..7572d44dae8 100644 --- a/homeassistant/components/brother/translations/pl.json +++ b/homeassistant/components/brother/translations/pl.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP drukarki", + "host": "[%key_id:common::config_flow::data::host%]", "type": "Typ drukarki" }, "description": "Konfiguracja integracji drukarek Brother. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/brother", diff --git a/homeassistant/components/brother/translations/ru.json b/homeassistant/components/brother/translations/ru.json index 66c4df1ac6a..57d84b17636 100644 --- a/homeassistant/components/brother/translations/ru.json +++ b/homeassistant/components/brother/translations/ru.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/brother.", diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index cf268cb4563..949f3c66854 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "\u5370\u8868\u6a5f\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "type": "\u5370\u8868\u6a5f\u985e\u578b" }, "description": "\u8a2d\u5b9a Brother \u5370\u8868\u6a5f\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/brother", diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index b3a007277c3..83c20ea1088 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -11,7 +11,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, - CoverDevice, + CoverEntity, ) from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class BruntDevice(CoverDevice): +class BruntDevice(CoverEntity): """ Representation of a Brunt cover device. diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py new file mode 100644 index 00000000000..c799f8daa9b --- /dev/null +++ b/homeassistant/components/bsblan/__init__.py @@ -0,0 +1,64 @@ +"""The BSB-Lan integration.""" +from datetime import timedelta +import logging + +from bsblan import BSBLan, BSBLanConnectionError + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN + +SCAN_INTERVAL = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the BSB-Lan component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up BSB-Lan from a config entry.""" + + session = async_get_clientsession(hass) + bsblan = BSBLan( + entry.data[CONF_HOST], + passkey=entry.data[CONF_PASSKEY], + loop=hass.loop, + port=entry.data[CONF_PORT], + session=session, + ) + + try: + await bsblan.info() + except BSBLanConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload BSBLan config entry.""" + + await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py new file mode 100644 index 00000000000..aaeb1fbffdb --- /dev/null +++ b/homeassistant/components/bsblan/climate.py @@ -0,0 +1,237 @@ +"""BSBLAN platform to control a compatible Climate Device.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, List, Optional + +from bsblan import BSBLan, BSBLanError, Info, State + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_ECO, + PRESET_NONE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_NAME, + ATTR_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_TARGET_TEMPERATURE, + DATA_BSBLAN_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=20) + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +HVAC_MODES = [ + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +] + +PRESET_MODES = [ + PRESET_ECO, + PRESET_NONE, +] + +HA_STATE_TO_BSBLAN = { + HVAC_MODE_AUTO: "1", + HVAC_MODE_HEAT: "3", + HVAC_MODE_OFF: "0", +} + +BSBLAN_TO_HA_STATE = {value: key for key, value in HA_STATE_TO_BSBLAN.items()} + +HA_PRESET_TO_BSBLAN = { + PRESET_ECO: "2", +} + +BSBLAN_TO_HA_PRESET = { + 2: PRESET_ECO, +} + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up BSBLan device based on a config entry.""" + bsblan: BSBLan = hass.data[DOMAIN][entry.entry_id][DATA_BSBLAN_CLIENT] + info = await bsblan.info() + async_add_entities([BSBLanClimate(entry.entry_id, bsblan, info)], True) + + +class BSBLanClimate(ClimateEntity): + """Defines a BSBLan climate device.""" + + def __init__( + self, entry_id: str, bsblan: BSBLan, info: Info, + ): + """Initialize BSBLan climate device.""" + self._current_temperature: Optional[float] = None + self._available = True + self._current_hvac_mode: Optional[int] = None + self._target_temperature: Optional[float] = None + self._info: Info = info + self.bsblan = bsblan + self._temperature_unit = None + self._hvac_mode = None + self._preset_mode = None + self._store_hvac_mode = None + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._info.device_identification + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._info.device_identification + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement which this thermostat uses.""" + if self._temperature_unit == "°C": + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_FLAGS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def hvac_mode(self): + """Return the current operation mode.""" + return self._current_hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return HVAC_MODES + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def preset_modes(self): + """List of available preset modes.""" + return PRESET_MODES + + @property + def preset_mode(self): + """Return the preset_mode.""" + return self._preset_mode + + async def async_set_preset_mode(self, preset_mode): + """Set preset mode.""" + _LOGGER.debug("Setting preset mode to: %s", preset_mode) + if preset_mode == PRESET_NONE: + # restore previous hvac mode + self._current_hvac_mode = self._store_hvac_mode + else: + # Store hvac mode. + self._store_hvac_mode = self._current_hvac_mode + await self.async_set_data(preset_mode=preset_mode) + + async def async_set_hvac_mode(self, hvac_mode): + """Set HVAC mode.""" + _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) + # preset should be none when hvac mode is set + self._preset_mode = PRESET_NONE + await self.async_set_data(hvac_mode=hvac_mode) + + async def async_set_temperature(self, **kwargs): + """Set new target temperatures.""" + await self.async_set_data(**kwargs) + + async def async_set_data(self, **kwargs: Any) -> None: + """Set device settings using BSBLan.""" + data = {} + + if ATTR_TEMPERATURE in kwargs: + data[ATTR_TARGET_TEMPERATURE] = kwargs[ATTR_TEMPERATURE] + _LOGGER.debug("Set temperature data = %s", data) + + if ATTR_HVAC_MODE in kwargs: + data[ATTR_HVAC_MODE] = HA_STATE_TO_BSBLAN[kwargs[ATTR_HVAC_MODE]] + _LOGGER.debug("Set hvac mode data = %s", data) + + if ATTR_PRESET_MODE in kwargs: + # for now we set the preset as hvac_mode as the api expect this + data[ATTR_HVAC_MODE] = HA_PRESET_TO_BSBLAN[kwargs[ATTR_PRESET_MODE]] + + try: + await self.bsblan.thermostat(**data) + except BSBLanError: + _LOGGER.error("An error occurred while updating the BSBLan device") + self._available = False + + async def async_update(self) -> None: + """Update BSBlan entity.""" + try: + state: State = await self.bsblan.state() + except BSBLanError: + if self._available: + _LOGGER.error("An error occurred while updating the BSBLan device") + self._available = False + return + + self._available = True + + self._current_temperature = float(state.current_temperature) + self._target_temperature = float(state.target_temperature) + + # check if preset is active else get hvac mode + _LOGGER.debug("state hvac/preset mode: %s", state.current_hvac_mode) + if state.current_hvac_mode == "2": + self._preset_mode = PRESET_ECO + else: + self._current_hvac_mode = BSBLAN_TO_HA_STATE[state.current_hvac_mode] + self._preset_mode = PRESET_NONE + + self._temperature_unit = state.temperature_unit + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this BSBLan device.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)}, + ATTR_NAME: "BSBLan Device", + ATTR_MANUFACTURER: "BSBLan", + ATTR_MODEL: self._info.controller_variant, + } diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py new file mode 100644 index 00000000000..ba5e7468832 --- /dev/null +++ b/homeassistant/components/bsblan/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for BSB-Lan integration.""" +import logging +from typing import Any, Dict, Optional + +from bsblan import BSBLan, BSBLanError, Info +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( # pylint:disable=unused-import + CONF_DEVICE_IDENT, + CONF_PASSKEY, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a BSBLan config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() + + try: + info = await self._get_bsblan_info( + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + passkey=user_input.get(CONF_PASSKEY), + ) + except BSBLanError: + return self._show_setup_form({"base": "connection_error"}) + + # Check if already configured + await self.async_set_unique_id(info.device_identification) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info.device_identification, + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_PASSKEY: user_input.get(CONF_PASSKEY), + CONF_DEVICE_IDENT: info.device_identification, + }, + ) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=80): int, + vol.Optional(CONF_PASSKEY): str, + } + ), + errors=errors or {}, + ) + + async def _get_bsblan_info( + self, host: str, passkey: Optional[str], port: int + ) -> Info: + """Get device information from an BSBLan device.""" + session = async_get_clientsession(self.hass) + _LOGGER.debug("request bsblan.info:") + bsblan = BSBLan( + host, passkey=passkey, port=port, session=session, loop=self.hass.loop + ) + return await bsblan.info() diff --git a/homeassistant/components/bsblan/const.py b/homeassistant/components/bsblan/const.py new file mode 100644 index 00000000000..1dd461e2081 --- /dev/null +++ b/homeassistant/components/bsblan/const.py @@ -0,0 +1,26 @@ +"""Constants for the BSB-Lan integration.""" + +DOMAIN = "bsblan" + +DATA_BSBLAN_CLIENT = "bsblan_client" +DATA_BSBLAN_TIMER = "bsblan_timer" +DATA_BSBLAN_UPDATED = "bsblan_updated" + +ATTR_IDENTIFIERS = "identifiers" +ATTR_MODEL = "model" +ATTR_MANUFACTURER = "manufacturer" + +ATTR_TARGET_TEMPERATURE = "target_temperature" +ATTR_INSIDE_TEMPERATURE = "inside_temperature" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" + +ATTR_STATE_ON = "on" +ATTR_STATE_OFF = "off" + +CONF_DEVICE_IDENT = "device_identification" +CONF_CONTROLLER_FAM = "controller_family" +CONF_CONTROLLER_VARI = "controller_variant" + +SENSOR_TYPE_TEMPERATURE = "temperature" + +CONF_PASSKEY = "passkey" diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json new file mode 100644 index 00000000000..e396db57962 --- /dev/null +++ b/homeassistant/components/bsblan/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "bsblan", + "name": "BSB-Lan", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bsblan", + "requirements": ["bsblan==0.3.7"], + "codeowners": ["@liudger"] +} diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json new file mode 100644 index 00000000000..2e7c63f4d3a --- /dev/null +++ b/homeassistant/components/bsblan/strings.json @@ -0,0 +1,23 @@ +{ + "title": "BSB-Lan", + "config": { + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "title": "Connect to the BSB-Lan device", + "description": "Set up you BSB-Lan device to integrate with Home Assistant.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "passkey": "Passkey string" + } + } + }, + "error": { + "connection_error": "Failed to connect to BSB-Lan device." + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ca.json b/homeassistant/components/bsblan/translations/ca.json new file mode 100644 index 00000000000..c906429568c --- /dev/null +++ b/homeassistant/components/bsblan/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "connection_error": "No s'ha pogut connectar amb el dispositiu BSB-Lan." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "passkey": "String Passkey", + "port": "N\u00famero de port" + }, + "title": "Connexi\u00f3 amb dispositiu BSB-Lan" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json new file mode 100644 index 00000000000..4515aa5f74d --- /dev/null +++ b/homeassistant/components/bsblan/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "host": "Server oder IP-Adresse", + "port": "Port Nummer" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/en.json b/homeassistant/components/bsblan/translations/en.json new file mode 100644 index 00000000000..ce745b351b1 --- /dev/null +++ b/homeassistant/components/bsblan/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "connection_error": "Failed to connect to BSB-Lan device." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "passkey": "Passkey string", + "port": "Port" + }, + "description": "Set up you BSB-Lan device to integrate with Home Assistant.", + "title": "Connect to the BSB-Lan device" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json new file mode 100644 index 00000000000..77c7c51e68b --- /dev/null +++ b/homeassistant/components/bsblan/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "connection_error": "No se ha podido conectar con el dispositivo BSB-Lan." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "passkey": "Clave de acceso", + "port": "N\u00famero de puerto" + }, + "description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.", + "title": "Conectar con el dispositivo BSB-Lan" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/fi.json b/homeassistant/components/bsblan/translations/fi.json new file mode 100644 index 00000000000..1ab0cc17429 --- /dev/null +++ b/homeassistant/components/bsblan/translations/fi.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty" + }, + "error": { + "connection_error": "Yhteyden muodostaminen BSB-Lan-laitteeseen ep\u00e4onnistui." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Palvelin tai IP-osoite", + "passkey": "Todentamisavaimen merkkijono", + "port": "Portti" + }, + "description": "Asenna BSB-Lan-laite integroitavaksi Home Assistant -sovellukseen.", + "title": "Yhdist\u00e4 BSB-Lan-laitteeseen" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json new file mode 100644 index 00000000000..5a89dc38022 --- /dev/null +++ b/homeassistant/components/bsblan/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te ou adresse IP", + "port": "Num\u00e9ro de port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/it.json b/homeassistant/components/bsblan/translations/it.json new file mode 100644 index 00000000000..176d582fed1 --- /dev/null +++ b/homeassistant/components/bsblan/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_error": "Impossibile connettersi al dispositivo BSB-Lan." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Host o indirizzo IP", + "passkey": "Stringa passkey", + "port": "Numero porta" + }, + "description": "Configura il tuo dispositivo BSB-Lan per l'integrazione con Home Assistant.", + "title": "Collegamento al dispositivo BSB-Lan" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ko.json b/homeassistant/components/bsblan/translations/ko.json new file mode 100644 index 00000000000..7392edf62b3 --- /dev/null +++ b/homeassistant/components/bsblan/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_error": "BSB-Lan \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "passkey": "\ud328\uc2a4\ud0a4 \ubb38\uc790\uc5f4", + "port": "\ud3ec\ud2b8" + }, + "description": "Home Assistant \uc5d0 BSB-Lan \uae30\uae30 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "BSB-Lan \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/lb.json b/homeassistant/components/bsblan/translations/lb.json new file mode 100644 index 00000000000..ef5e25a1d08 --- /dev/null +++ b/homeassistant/components/bsblan/translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "connection_error": "Feeler beim verbannen mam BSB-Lan Apparat." + }, + "flow_title": "BSB-LAN: {name}", + "step": { + "user": { + "data": { + "host": "Numm oder IP Adresse", + "passkey": "Passkey Zeechefolleg", + "port": "Port Nummer" + }, + "description": "BSB-Lan Apparat ariichten fir d'Integratioun mam Home Assistant.", + "title": "Mam BSB-Lan Apparat verbannen" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json new file mode 100644 index 00000000000..a90cc5536b5 --- /dev/null +++ b/homeassistant/components/bsblan/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "connection_error": "Kunne ikke koble til BSB-Lan-enheten." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Vert eller IP-adresse", + "passkey": "Tilgangsn\u00f8kkel streng", + "port": "Portnummer" + }, + "description": "Konfigurer din BSB-Lan-enhet for \u00e5 integrere med Home Assistant.", + "title": "Koble til BSB-Lan-enheten" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json new file mode 100644 index 00000000000..4a53a352896 --- /dev/null +++ b/homeassistant/components/bsblan/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json new file mode 100644 index 00000000000..60d0ad4b653 --- /dev/null +++ b/homeassistant/components/bsblan/translations/ru.json @@ -0,0 +1,23 @@ +{ + "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": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "passkey": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BSB-Lan.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json new file mode 100644 index 00000000000..09f15ff84f8 --- /dev/null +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_error": "BSB-Lan \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "BSB-Lan\uff1a{name}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "passkey": "Passkey \u5b57\u4e32", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a BSB-Lan \u8a2d\u5099\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7dda\u81f3 BSB-Lan \u8a2d\u5099" + } + } + }, + "title": "BSB-Lan" +} \ No newline at end of file diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 579755709d1..3691f704a13 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -174,7 +174,7 @@ class WebDavCalendarData: uid = vevent.uid.value data = { "uid": uid, - "title": vevent.summary.value, + "summary": vevent.summary.value, "start": self.get_hass_date(vevent.dtstart.value), "end": self.get_hass_date(self.get_end_date(vevent)), "location": self.get_attr_value(vevent, "location"), diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 6f7427a0234..3528608dde3 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -35,9 +35,9 @@ async def async_setup(hass, config): hass.http.register_view(CalendarListView(component)) hass.http.register_view(CalendarEventView(component)) - # Doesn't work in prod builds of the frontend: home-assistant-polymer#1289 - # hass.components.frontend.async_register_built_in_panel( - # 'calendar', 'calendar', 'hass:calendar') + hass.components.frontend.async_register_built_in_panel( + "calendar", "calendar", "hass:calendar" + ) await component.async_setup(config) return True diff --git a/homeassistant/components/calendar/translations/no.json b/homeassistant/components/calendar/translations/no.json index ba22ae540fc..516a3b7d443 100644 --- a/homeassistant/components/calendar/translations/no.json +++ b/homeassistant/components/calendar/translations/no.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, "title": "Kalender" } \ No newline at end of file diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2862805a333..0b2c1e77d3f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -46,6 +46,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass from homeassistant.setup import async_when_setup @@ -684,7 +685,7 @@ async def async_handle_play_stream_service(camera, service_call): ) data = { ATTR_ENTITY_ID: entity_ids, - ATTR_MEDIA_CONTENT_ID: f"{hass.config.api.base_url}{url}", + ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } diff --git a/homeassistant/components/camera/translations/it.json b/homeassistant/components/camera/translations/it.json index 79fe9916de3..a5f4be7967e 100644 --- a/homeassistant/components/camera/translations/it.json +++ b/homeassistant/components/camera/translations/it.json @@ -3,7 +3,7 @@ "_": { "idle": "Inattiva", "recording": "In registrazione", - "streaming": "Streaming" + "streaming": "In trasmissione" } }, "title": "Telecamera" diff --git a/homeassistant/components/camera/translations/no.json b/homeassistant/components/camera/translations/no.json index 6c2dc281761..9960881c81e 100644 --- a/homeassistant/components/camera/translations/no.json +++ b/homeassistant/components/camera/translations/no.json @@ -1,6 +1,7 @@ { "state": { "_": { + "idle": "Inaktiv", "recording": "Opptak", "streaming": "Str\u00f8mming" } diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 43290e9a345..ea0e3078b0c 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -3,7 +3,7 @@ import logging from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT -from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class CanaryAlarm(AlarmControlPanel): +class CanaryAlarm(AlarmControlPanelEntity): """Representation of a Canary alarm control panel.""" def __init__(self, data, location_id): diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index c933136d140..1a57e5c2dab 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import auth, config_entries, core from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import config_validation as cv, dispatcher +from homeassistant.helpers.network import get_url from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW @@ -40,15 +41,7 @@ async def async_setup_ha_cast( async def handle_show_view(call: core.ServiceCall): """Handle a Show View service call.""" - hass_url = hass.config.api.base_url - - # Home Assistant Cast only works with https urls. If user has no configured - # base url, use their remote url. - if not hass_url.lower().startswith("https://"): - try: - hass_url = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + hass_url = get_url(hass, require_ssl=True) controller = HomeAssistantController( # If you are developing Home Assistant Cast, uncomment and set to your dev app id. diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b8ad1fe67cc..ddb6697d370 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==5.0.0"], + "requirements": ["pychromecast==5.1.0"], "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c1729850189..fd7443d1c26 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,18 +1,20 @@ """Provide functionality to interact with Cast devices on the network.""" import asyncio +import json import logging from typing import Optional import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController from pychromecast.controllers.multizone import MultizoneManager +from pychromecast.quick_play import quick_play from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, ) import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -171,7 +173,7 @@ async def _async_setup_platform( hass.async_add_executor_job(setup_internal_discovery, hass) -class CastDevice(MediaPlayerDevice): +class CastDevice(MediaPlayerEntity): """Representation of a Cast device on the network. This class is the holder of the pychromecast.Chromecast object and its @@ -477,7 +479,33 @@ class CastDevice(MediaPlayerDevice): def play_media(self, media_type, media_id, **kwargs): """Play media from a URL.""" # We do not want this to be forwarded to a group - self._chromecast.media_controller.play_media(media_id, media_type) + if media_type == CAST_DOMAIN: + try: + app_data = json.loads(media_id) + except json.JSONDecodeError: + _LOGGER.error("Invalid JSON in media_content_id") + raise + + # Special handling for passed `app_id` parameter. This will only launch + # an arbitrary cast app, generally for UX. + if "app_id" in app_data: + app_id = app_data.pop("app_id") + _LOGGER.info("Starting Cast app by ID %s", app_id) + self._chromecast.start_app(app_id) + if app_data: + _LOGGER.warning( + "Extra keys %s were ignored. Please use app_name to cast media.", + app_data.keys(), + ) + return + + app_name = app_data.pop("app_name") + try: + quick_play(self._chromecast, app_name, app_data) + except NotImplementedError: + _LOGGER.error("App %s not supported", app_name) + else: + self._chromecast.media_controller.play_media(media_id, media_type) # ========== Properties ========== @property diff --git a/homeassistant/components/cast/translations/fi.json b/homeassistant/components/cast/translations/fi.json new file mode 100644 index 00000000000..21ec8206165 --- /dev/null +++ b/homeassistant/components/cast/translations/fi.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Verkosta ei l\u00f6ydy Google Cast -laitteita.", + "single_instance_allowed": "Vain yksi Google Cast -m\u00e4\u00e4ritys on tarpeen." + }, + "step": { + "confirm": { + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 Google Castin?", + "title": "Google Cast" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 39ec2c35ac7..ec1e9110317 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, TIME_DAYS, @@ -27,15 +26,11 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=12) -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_NAME, invalidation_version="0.109"), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } - ), +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } ) diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index f456809147b..fb14e90d586 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -6,8 +6,8 @@ "title": "Define the certificate to test", "data": { "name": "The name of the certificate", - "host": "The hostname of the certificate", - "port": "The port of the certificate" + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -21,4 +21,4 @@ "import_failed": "Import from config failed" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/en.json b/homeassistant/components/cert_expiry/translations/en.json index 5844868a6e4..8d551ec2e03 100644 --- a/homeassistant/components/cert_expiry/translations/en.json +++ b/homeassistant/components/cert_expiry/translations/en.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "The hostname of the certificate", + "host": "Host", "name": "The name of the certificate", - "port": "The port of the certificate" + "port": "Port" }, "title": "Define the certificate to test" } diff --git a/homeassistant/components/cert_expiry/translations/es-419.json b/homeassistant/components/cert_expiry/translations/es-419.json index ee5fc92391f..2809d3c2899 100644 --- a/homeassistant/components/cert_expiry/translations/es-419.json +++ b/homeassistant/components/cert_expiry/translations/es-419.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", + "import_failed": "La importaci\u00f3n desde la configuraci\u00f3n fall\u00f3" + }, "error": { + "connection_refused": "Conexi\u00f3n rechazada al conectarse al host", "connection_timeout": "Tiempo de espera al conectarse a este host", "resolve_failed": "Este host no puede resolverse" }, diff --git a/homeassistant/components/cert_expiry/translations/ko.json b/homeassistant/components/cert_expiry/translations/ko.json index 699ca413604..ee912a33695 100644 --- a/homeassistant/components/cert_expiry/translations/ko.json +++ b/homeassistant/components/cert_expiry/translations/ko.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "\uc778\uc99d\uc11c\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984", + "host": "\ud638\uc2a4\ud2b8", "name": "\uc778\uc99d\uc11c\uc758 \uc774\ub984", - "port": "\uc778\uc99d\uc11c\uc758 \ud3ec\ud2b8" + "port": "\ud3ec\ud2b8" }, "title": "\uc778\uc99d\uc11c \uc815\uc758 \ud14c\uc2a4\ud2b8 \ub300\uc0c1" } diff --git a/homeassistant/components/cert_expiry/translations/nl.json b/homeassistant/components/cert_expiry/translations/nl.json index c33d4c06e6f..d844d28e62f 100644 --- a/homeassistant/components/cert_expiry/translations/nl.json +++ b/homeassistant/components/cert_expiry/translations/nl.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Deze combinatie van host en poort is al geconfigureerd", + "import_failed": "Importeren vanuit configuratie is mislukt" + }, "error": { + "connection_refused": "Verbinding geweigerd bij verbinding met host", "connection_timeout": "Time-out bij verbinding maken met deze host", "resolve_failed": "Deze host kon niet gevonden worden" }, diff --git a/homeassistant/components/cert_expiry/translations/pl.json b/homeassistant/components/cert_expiry/translations/pl.json index f213befed4f..b41dbdf9622 100644 --- a/homeassistant/components/cert_expiry/translations/pl.json +++ b/homeassistant/components/cert_expiry/translations/pl.json @@ -14,7 +14,7 @@ "data": { "host": "Nazwa hosta certyfikatu", "name": "Nazwa certyfikatu", - "port": "Port certyfikatu" + "port": "[%key_id:common::config_flow::data::port%] certyfikatu" }, "title": "Zdefiniuj certyfikat do przetestowania" } diff --git a/homeassistant/components/cert_expiry/translations/ru.json b/homeassistant/components/cert_expiry/translations/ru.json index 5219caa057d..a924398f90e 100644 --- a/homeassistant/components/cert_expiry/translations/ru.json +++ b/homeassistant/components/cert_expiry/translations/ru.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f", + "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/cert_expiry/translations/sv.json b/homeassistant/components/cert_expiry/translations/sv.json index 8449db1ec7a..23703f11e5b 100644 --- a/homeassistant/components/cert_expiry/translations/sv.json +++ b/homeassistant/components/cert_expiry/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "connection_refused": "Anslutningen blev tillbakavisad under anslutning till v\u00e4rd.", "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden", "resolve_failed": "Denna v\u00e4rd kan inte resolveras" }, diff --git a/homeassistant/components/cert_expiry/translations/zh-Hant.json b/homeassistant/components/cert_expiry/translations/zh-Hant.json index 1968f3d866c..6cfe05f6d1a 100644 --- a/homeassistant/components/cert_expiry/translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/translations/zh-Hant.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "\u8a8d\u8b49\u4e3b\u6a5f\u7aef\u540d\u7a31", + "host": "\u4e3b\u6a5f\u7aef", "name": "\u8a8d\u8b49\u540d\u7a31", - "port": "\u8a8d\u8b49\u901a\u8a0a\u57e0" + "port": "\u901a\u8a0a\u57e0" }, "title": "\u5b9a\u7fa9\u8a8d\u8b49\u9032\u884c\u6e2c\u8a66" } diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index fb5f8cb6ac0..65be051ad17 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -4,7 +4,7 @@ import logging from pychannels import Channels import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, @@ -116,7 +116,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ChannelsPlayer(MediaPlayerDevice): +class ChannelsPlayer(MediaPlayerEntity): """Representation of a Channels instance.""" def __init__(self, name, host, port): diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index db4dfc38664..7478c9a7f2b 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -6,7 +6,7 @@ import time from clementineremote import ClementineRemote import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ClementineDevice(client, config[CONF_NAME])]) -class ClementineDevice(MediaPlayerDevice): +class ClementineDevice(MediaPlayerEntity): """Representation of Clementine Player.""" def __init__(self, client, name): diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f3aff44ff4d..d3241791cf2 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -101,7 +101,7 @@ SET_TEMPERATURE_SCHEMA = vol.All( async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up climate devices.""" + """Set up climate entities.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -156,8 +156,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class ClimateDevice(Entity): - """Representation of a climate device.""" +class ClimateEntity(Entity): + """Representation of a climate entity.""" @property def state(self) -> str: @@ -509,7 +509,7 @@ class ClimateDevice(Entity): async def async_service_aux_heat( - entity: ClimateDevice, service: ServiceDataType + entity: ClimateEntity, service: ServiceDataType ) -> None: """Handle aux heat service.""" if service.data[ATTR_AUX_HEAT]: @@ -519,7 +519,7 @@ async def async_service_aux_heat( async def async_service_temperature_set( - entity: ClimateDevice, service: ServiceDataType + entity: ClimateEntity, service: ServiceDataType ) -> None: """Handle set temperature service.""" hass = entity.hass @@ -534,3 +534,15 @@ async def async_service_temperature_set( kwargs[value] = temp await entity.async_set_temperature(**kwargs) + + +class ClimateDevice(ClimateEntity): + """Representation of a climate entity (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "ClimateDevice is deprecated, modify %s to extend ClimateEntity", + cls.__name__, + ) diff --git a/homeassistant/components/climate/translations/es-419.json b/homeassistant/components/climate/translations/es-419.json index d61483edda2..569d5766f74 100644 --- a/homeassistant/components/climate/translations/es-419.json +++ b/homeassistant/components/climate/translations/es-419.json @@ -1,10 +1,17 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}" + "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}", + "set_preset_mode": "Cambiar el ajuste preestablecido en el valor de {entity_name}" }, "condition_type": { - "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico" + "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", + "is_preset_mode": "{entity_name} est\u00e1 configurado en un modo preestablecido espec\u00edfico" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} ha cambiado la humedad medida", + "current_temperature_changed": "{entity_name} ha cambiado la temperatura medida", + "hvac_mode_changed": "{entity_name} modo HVAC cambi\u00f3" } }, "state": { diff --git a/homeassistant/components/climate/translations/es.json b/homeassistant/components/climate/translations/es.json index 9ed5ff150c0..a7258609d59 100644 --- a/homeassistant/components/climate/translations/es.json +++ b/homeassistant/components/climate/translations/es.json @@ -19,7 +19,7 @@ "auto": "Autom\u00e1tico", "cool": "Fr\u00edo", "dry": "Seco", - "fan_only": "S\u00f3lo ventilador", + "fan_only": "Solo ventilador", "heat": "Calor", "heat_cool": "Calor/Fr\u00edo", "off": "Apagado" diff --git a/homeassistant/components/climate/translations/no.json b/homeassistant/components/climate/translations/no.json index 4e9722bb207..4ac58d07bbb 100644 --- a/homeassistant/components/climate/translations/no.json +++ b/homeassistant/components/climate/translations/no.json @@ -1,27 +1,28 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "Endre HVAC-modus p\u00e5 {entity_name}", - "set_preset_mode": "Endre forh\u00e5ndsinnstilling p\u00e5 {entity_name}" + "set_hvac_mode": "Endre klima-modus p\u00e5 {entity_name}", + "set_preset_mode": "Endre modus p\u00e5 {entity_name}" }, "condition_type": { - "is_hvac_mode": "{entity_name} er satt til en spesifikk HVAC-modus", + "is_hvac_mode": "{entity_name} er satt til en spesifikk klima-modus", "is_preset_mode": "{entity_name} er satt til en spesifikk forh\u00e5ndsinnstilt modus" }, "trigger_type": { "current_humidity_changed": "{entity_name} m\u00e5lt fuktighet er endret", "current_temperature_changed": "{entity_name} m\u00e5lt temperatur er endret", - "hvac_mode_changed": "{entity_name} HVAC-modus er endret" + "hvac_mode_changed": "{entity_name} klima-modus er endret" } }, "state": { "_": { - "auto": "Auto", + "auto": "", "cool": "Kj\u00f8le", "dry": "T\u00f8rr", "fan_only": "Kun vifte", "heat": "Varme", - "heat_cool": "Varme/kj\u00f8lig" + "heat_cool": "Varme/kj\u00f8lig", + "off": "Av" } }, "title": "Klima" diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 1d0de26918d..4a3a2dd77f8 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -3,6 +3,7 @@ import asyncio import logging from typing import Any +import aiohttp from hass_nabucasa import account_link from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION @@ -73,7 +74,10 @@ async def _get_services(hass): if services is not None: return services - services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) + try: + services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) + except (aiohttp.ClientError, asyncio.TimeoutError): + return [] hass.data[DATA_SERVICES] = services diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index c2974678faa..baa63679d42 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Home Assistant Cloud binary sensors.""" import asyncio -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN @@ -18,7 +18,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([CloudRemoteBinary(cloud)]) -class CloudRemoteBinary(BinarySensorDevice): +class CloudRemoteBinary(BinarySensorEntity): """Representation of an Cloud Remote UI Connection binary sensor.""" def __init__(self, cloud): diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index e9b9513479f..73a55fda8e3 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -4,7 +4,7 @@ import logging from pycmus import exceptions, remote import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -72,7 +72,7 @@ def setup_platform(hass, config, add_entities, discover_info=None): add_entities([cmus_remote], True) -class CmusDevice(MediaPlayerDevice): +class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" # pylint: disable=no-member diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index eaa371be1a3..dc62d8daa9d 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import ( CONF_COMMAND, @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class CommandBinarySensor(BinarySensorDevice): +class CommandBinarySensor(BinarySensorEntity): """Representation of a command line binary sensor.""" def __init__( diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 1edf141604f..6f2a038d051 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,7 +4,7 @@ import subprocess import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, @@ -63,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(covers) -class CommandCover(CoverDevice): +class CommandCover(CoverEntity): """Representation a command line cover.""" def __init__( diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index f89ac6f5b92..7f62970b639 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - SwitchDevice, + SwitchEntity, ) from homeassistant.const import ( CONF_COMMAND_OFF, @@ -66,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class CommandSwitch(SwitchDevice): +class CommandSwitch(SwitchEntity): """Representation a switch that can be toggled using shell commands.""" def __init__( diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index afb7e23e8fc..94880dcccf7 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) -class Concord232Alarm(alarm.AlarmControlPanel): +class Concord232Alarm(alarm.AlarmControlPanelEntity): """Representation of the Concord232-based alarm panel.""" def __init__(self, url, name, code, mode): diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 326ac799f06..3077056c397 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -95,7 +95,7 @@ def get_opening_type(zone): return "opening" -class Concord232ZoneSensor(BinarySensorDevice): +class Concord232ZoneSensor(BinarySensorEntity): """Representation of a Concord232 zone as a sensor.""" def __init__(self, hass, client, zone, zone_type): diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index e9ceb7eac57..89f4edc95d6 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -44,6 +44,8 @@ class CheckConfigView(HomeAssistantView): vol.Optional("unit_system"): cv.unit_system, vol.Optional("location_name"): str, vol.Optional("time_zone"): cv.time_zone, + vol.Optional("external_url"): vol.Any(cv.url, None), + vol.Optional("internal_url"): vol.Any(cv.url, None), } ) async def websocket_update_config(hass, connection, msg): diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 08f53f948fe..5b12ccb92eb 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -71,6 +71,7 @@ def _entry_dict(entry): "model": entry.model, "name": entry.name, "sw_version": entry.sw_version, + "entry_type": entry.entry_type, "id": entry.id, "via_device_id": entry.via_device_id, "area_id": entry.area_id, diff --git a/homeassistant/components/configurator/translations/it.json b/homeassistant/components/configurator/translations/it.json index 3e17f84d1c8..b8610b76d9d 100644 --- a/homeassistant/components/configurator/translations/it.json +++ b/homeassistant/components/configurator/translations/it.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Configura", + "configure": "Configurare", "configured": "Configurato" } }, diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index a52431dd89b..6e68e858a6d 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -4,7 +4,7 @@ import logging from pycoolmasternet import CoolMasterNet -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -60,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices): async_add_devices(all_devices, True) -class CoolmasterClimate(ClimateDevice): +class CoolmasterClimate(ClimateEntity): """Representation of a coolmaster climate device.""" def __init__(self, device, supported_modes): diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json index 3bb5d3ad4e1..fd91e5576d2 100644 --- a/homeassistant/components/coolmaster/strings.json +++ b/homeassistant/components/coolmaster/strings.json @@ -4,7 +4,7 @@ "user": { "title": "Setup your CoolMasterNet connection details.", "data": { - "host": "Host", + "host": "[%key:common::config_flow::data::host%]", "off": "Can be turned off", "heat": "Support heat mode", "cool": "Support cool mode", @@ -19,4 +19,4 @@ "no_units": "Could not find any HVAC units in CoolMasterNet host." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/es-419.json b/homeassistant/components/coolmaster/translations/es-419.json index e1da9263a0c..8cdf9675fd2 100644 --- a/homeassistant/components/coolmaster/translations/es-419.json +++ b/homeassistant/components/coolmaster/translations/es-419.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "connection_error": "Error al conectarse a la instancia de CoolMasterNet. Por favor revise su host.", + "no_units": "No se encontraron unidades de HVAC en el host CoolMasterNet." + }, "step": { "user": { "data": { + "host": "Host", "off": "Puede ser apagado" }, "title": "Configure los detalles de su conexi\u00f3n CoolMasterNet." diff --git a/homeassistant/components/coolmaster/translations/pl.json b/homeassistant/components/coolmaster/translations/pl.json index 9b0e4bc5846..0edbd6ed379 100644 --- a/homeassistant/components/coolmaster/translations/pl.json +++ b/homeassistant/components/coolmaster/translations/pl.json @@ -12,7 +12,7 @@ "fan_only": "Obs\u0142uga trybu \"tylko wentylator\"", "heat": "Obs\u0142uga trybu grzania", "heat_cool": "Obs\u0142uga automatycznego trybu grzanie/ch\u0142odzenie", - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone" }, "title": "Skonfiguruj szczeg\u00f3\u0142y po\u0142\u0105czenia CoolMasterNet." diff --git a/homeassistant/components/coronavirus/translations/es-419.json b/homeassistant/components/coronavirus/translations/es-419.json new file mode 100644 index 00000000000..1a1139a8f31 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Este pa\u00eds ya est\u00e1 configurado." + }, + "step": { + "user": { + "data": { + "country": "Pa\u00eds" + }, + "title": "Seleccione un pa\u00eds para monitorear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/ko.json b/homeassistant/components/coronavirus/translations/ko.json index e549a674693..65eec9e8bb7 100644 --- a/homeassistant/components/coronavirus/translations/ko.json +++ b/homeassistant/components/coronavirus/translations/ko.json @@ -8,7 +8,7 @@ "data": { "country": "\uad6d\uac00" }, - "title": "\ubaa8\ub2c8\ud130\ub9c1 \ud560 \uad6d\uac00\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" + "title": "\ubaa8\ub2c8\ud130\ub9c1 \ud560 \uad6d\uac00\ub97c \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/coronavirus/translations/sv.json b/homeassistant/components/coronavirus/translations/sv.json new file mode 100644 index 00000000000..7e6686c2a04 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Detta land \u00e4r redan konfigurerat." + }, + "step": { + "user": { + "data": { + "country": "Land" + }, + "title": "V\u00e4lj ett land att \u00f6vervaka" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index cb2812f319b..ef17abc8a40 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -94,10 +94,12 @@ async def async_setup(hass, config): await component.async_setup(config) - component.async_register_entity_service(SERVICE_OPEN_COVER, {}, "async_open_cover") + component.async_register_entity_service( + SERVICE_OPEN_COVER, {}, "async_open_cover", [SUPPORT_OPEN] + ) component.async_register_entity_service( - SERVICE_CLOSE_COVER, {}, "async_close_cover" + SERVICE_CLOSE_COVER, {}, "async_close_cover", [SUPPORT_CLOSE] ) component.async_register_entity_service( @@ -108,22 +110,27 @@ async def async_setup(hass, config): ) }, "async_set_cover_position", - ) - - component.async_register_entity_service(SERVICE_STOP_COVER, {}, "async_stop_cover") - - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - - component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt" + [SUPPORT_SET_POSITION], ) component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt" + SERVICE_STOP_COVER, {}, "async_stop_cover", [SUPPORT_STOP] ) component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt" + SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_OPEN | SUPPORT_CLOSE] + ) + + component.async_register_entity_service( + SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt", [SUPPORT_OPEN_TILT] + ) + + component.async_register_entity_service( + SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt", [SUPPORT_CLOSE_TILT] + ) + + component.async_register_entity_service( + SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt", [SUPPORT_STOP_TILT] ) component.async_register_entity_service( @@ -134,10 +141,14 @@ async def async_setup(hass, config): ) }, "async_set_cover_tilt_position", + [SUPPORT_SET_TILT_POSITION], ) component.async_register_entity_service( - SERVICE_TOGGLE_COVER_TILT, {}, "async_toggle_tilt" + SERVICE_TOGGLE_COVER_TILT, + {}, + "async_toggle_tilt", + [SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT], ) return True @@ -153,7 +164,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class CoverDevice(Entity): +class CoverEntity(Entity): """Representation of a cover.""" @property @@ -318,3 +329,14 @@ class CoverDevice(Entity): await self.async_open_cover_tilt(**kwargs) else: await self.async_close_cover_tilt(**kwargs) + + +class CoverDevice(CoverEntity): + """Representation of a cover (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "CoverDevice is deprecated, modify %s to extend CoverEntity", cls.__name__, + ) diff --git a/homeassistant/components/cover/translations/es-419.json b/homeassistant/components/cover/translations/es-419.json index 3593ba28960..c6f9f7db7dd 100644 --- a/homeassistant/components/cover/translations/es-419.json +++ b/homeassistant/components/cover/translations/es-419.json @@ -1,4 +1,30 @@ { + "device_automation": { + "action_type": { + "close": "Cerrar {entity_name}", + "close_tilt": "Cerrar la inclinaci\u00f3n de {entity_name}", + "open": "Abrir {entity_name}", + "open_tilt": "Abrir la inclinaci\u00f3n de {entity_name}", + "set_position": "Establecer la posici\u00f3n de {entity_name}", + "set_tilt_position": "Establecer la posici\u00f3n de inclinaci\u00f3n {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} est\u00e1 cerrado", + "is_closing": "{entity_name} se est\u00e1 cerrando", + "is_open": "{entity_name} est\u00e1 abierto", + "is_opening": "{entity_name} se est\u00e1 abriendo", + "is_position": "La posici\u00f3n actual de {entity_name} es", + "is_tilt_position": "La posici\u00f3n de inclinaci\u00f3n actual de {entity_name} es" + }, + "trigger_type": { + "closed": "{entity_name} cerrado", + "closing": "{entity_name} cerrando", + "opened": "{entity_name} abierto", + "opening": "{entity_name} abriendo", + "position": "Cambios de posici\u00f3n de {entity_name}", + "tilt_position": "Cambios en la posici\u00f3n de inclinaci\u00f3n cambi\u00f3 de {entity_name}" + } + }, "state": { "_": { "closed": "Cerrado", diff --git a/homeassistant/components/cover/translations/fr.json b/homeassistant/components/cover/translations/fr.json index d9ceb569753..63fbdd80b84 100644 --- a/homeassistant/components/cover/translations/fr.json +++ b/homeassistant/components/cover/translations/fr.json @@ -1,7 +1,9 @@ { "device_automation": { "action_type": { - "close": "Fermer {entity_name}" + "close": "Fermer {entity_name}", + "open": "Ouvrir {entity_name}", + "set_position": "D\u00e9finir la position de {entity_name}" }, "condition_type": { "is_closed": "{entity_name} est ferm\u00e9", diff --git a/homeassistant/components/cover/translations/it.json b/homeassistant/components/cover/translations/it.json index 70589da242c..95f2e34d8eb 100644 --- a/homeassistant/components/cover/translations/it.json +++ b/homeassistant/components/cover/translations/it.json @@ -34,5 +34,5 @@ "stopped": "Arrestato" } }, - "title": "Chiusure" + "title": "Scuri" } \ No newline at end of file diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index f29132c3f18..7d68d78641e 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "Sluit {entity_name}", + "close_tilt": "Sluit de kanteling van {entity_name}", + "open": "Open {entity_name}", + "open_tilt": "Open de kanteling {entity_name}", + "set_position": "Stel de positie van {entity_name} in", + "set_tilt_position": "Stel de {entity_name} kantelpositie in" + }, "condition_type": { "is_closed": "{entity_name} is gesloten", "is_closing": "{entity_name} wordt gesloten", diff --git a/homeassistant/components/cover/translations/no.json b/homeassistant/components/cover/translations/no.json index 4d898ec75f8..eaa0f2d1678 100644 --- a/homeassistant/components/cover/translations/no.json +++ b/homeassistant/components/cover/translations/no.json @@ -9,25 +9,27 @@ "set_tilt_position": "Angi {entity_name} tilt posisjon" }, "condition_type": { - "is_closed": "{entity_name} er stengt", - "is_closing": "{entity_name} stenges", + "is_closed": "{entity_name} er lukket", + "is_closing": "{entity_name} lukker", "is_open": "{entity_name} er \u00e5pen", - "is_opening": "{entity_name} \u00e5pnes", - "is_position": "{entity_name}-posisjonen er", - "is_tilt_position": "{entity_name} vippeposisjon er" + "is_opening": "{entity_name} \u00e5pner", + "is_position": "N\u00e5v\u00e6rende {entity_name} posisjon er", + "is_tilt_position": "N\u00e5v\u00e6rende {entity_name} tilt posisjon er" }, "trigger_type": { "closed": "{entity_name} lukket", - "closing": "{entity_name} lukkes", + "closing": "{entity_name} lukker", "opened": "{entity_name} \u00e5pnet", - "opening": "{entity_name} \u00e5pning", + "opening": "{entity_name} \u00e5pner", "position": "{entity_name} posisjon endringer", - "tilt_position": "{entity_name} endringer i vippeposisjon" + "tilt_position": "{entity_name} tilt posisjon endringer" } }, "state": { "_": { + "closed": "Lukket", "closing": "Lukker", + "open": "\u00c5pen", "opening": "\u00c5pner", "stopped": "Stoppet" } diff --git a/homeassistant/components/cover/translations/pl.json b/homeassistant/components/cover/translations/pl.json index 501b2f78d7a..74f8d3860cb 100644 --- a/homeassistant/components/cover/translations/pl.json +++ b/homeassistant/components/cover/translations/pl.json @@ -9,20 +9,20 @@ "set_tilt_position": "ustaw pochylenie {entity_name}" }, "condition_type": { - "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", + "is_closed": "{entity_name} jest zamkni\u0119ta", "is_closing": "{entity_name} si\u0119 zamyka", - "is_open": "pokrywa {entity_name} jest otwarta", + "is_open": "{entity_name} jest otwarta", "is_opening": "{entity_name} si\u0119 otwiera", - "is_position": "pozycja pokrywy {entity_name} to", - "is_tilt_position": "pochylenie pokrywy {entity_name} to" + "is_position": "pozycja {entity_name} to", + "is_tilt_position": "pochylenie {entity_name} to" }, "trigger_type": { "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", "closing": "{entity_name} si\u0119 zamyka", "opened": "nast\u0105pi otwarcie {entity_name}", "opening": "{entity_name} si\u0119 otwiera", - "position": "zmieni si\u0119 pozycja pokrywy {entity_name}", - "tilt_position": "zmieni si\u0119 pochylenie pokrywy {entity_name}" + "position": "zmieni si\u0119 pozycja {entity_name}", + "tilt_position": "zmieni si\u0119 pochylenie {entity_name}" } }, "state": { @@ -31,8 +31,8 @@ "closing": "zamykanie", "open": "otwarta", "opening": "otwieranie", - "stopped": "zatrzymany" + "stopped": "zatrzymanie" } }, - "title": "Pokrywa" + "title": "Roleta" } \ No newline at end of file diff --git a/homeassistant/components/cover/translations/ru.json b/homeassistant/components/cover/translations/ru.json index df35c58b7dd..53d646fc09f 100644 --- a/homeassistant/components/cover/translations/ru.json +++ b/homeassistant/components/cover/translations/ru.json @@ -2,7 +2,9 @@ "device_automation": { "action_type": { "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c {entity_name}", + "close_tilt": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c {entity_name} \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u0430\u043a\u043b\u043e\u043d\u0430", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c {entity_name}", + "open_tilt": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c {entity_name} \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u0430\u043a\u043b\u043e\u043d\u0430", "set_position": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 {entity_name}", "set_tilt_position": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0430 {entity_name}" }, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index ad4d30358c2..1e03938dfcf 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -5,19 +5,18 @@ import logging from aiohttp import ClientConnectionError from async_timeout import timeout -from pydaikin.appliance import Appliance +from pydaikin.daikin_base import Appliance import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_HOSTS +from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_PASSWORD from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from . import config_flow # noqa: F401 -from .const import TIMEOUT +from .const import CONF_KEY, CONF_UUID, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -62,7 +61,13 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Establish connection with Daikin.""" conf = entry.data - daikin_api = await daikin_api_setup(hass, conf[CONF_HOST]) + daikin_api = await daikin_api_setup( + hass, + conf[CONF_HOST], + conf.get(CONF_KEY), + conf.get(CONF_UUID), + conf.get(CONF_PASSWORD), + ) if not daikin_api: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) @@ -87,14 +92,15 @@ async def async_unload_entry(hass, config_entry): return True -async def daikin_api_setup(hass, host): +async def daikin_api_setup(hass, host, key, uuid, password): """Create a Daikin instance only once.""" session = hass.helpers.aiohttp_client.async_get_clientsession() try: with timeout(TIMEOUT): - device = Appliance(host, session) - await device.init() + device = await Appliance.factory( + host, session, key=key, uuid=uuid, password=password + ) except asyncio.TimeoutError: _LOGGER.debug("Connection to %s timed out", host) raise ConfigEntryNotReady @@ -113,11 +119,11 @@ async def daikin_api_setup(hass, host): class DaikinApi: """Keep the Daikin instance in one place and centralize the update.""" - def __init__(self, device): + def __init__(self, device: Appliance): """Initialize the Daikin Handle.""" self.device = device - self.name = device.values["name"] - self.ip_address = device.ip + self.name = device.values.get("name", "Daikin AC") + self.ip_address = device.device_ip self._available = True @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -135,20 +141,14 @@ class DaikinApi: """Return True if entity is available.""" return self._available - @property - def mac(self): - """Return mac-address of device.""" - return self.device.values.get(CONNECTION_NETWORK_MAC) - @property def device_info(self): """Return a device description for device registry.""" info = self.device.values return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, - "identifieres": self.mac, + "identifieres": self.device.mac, "manufacturer": "Daikin", "model": info.get("model"), "name": info.get("name"), - "sw_version": info.get("ver").replace("_", "."), + "sw_version": info.get("ver", "").replace("_", "."), } diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 5455bd6f670..60a126c182b 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -1,10 +1,9 @@ """Support for the Daikin HVAC.""" import logging -from pydaikin import appliance import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, @@ -86,7 +85,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities([DaikinClimate(daikin_api)], update_before_add=True) -class DaikinClimate(ClimateDevice): +class DaikinClimate(ClimateEntity): """Representation of a Daikin HVAC.""" def __init__(self, api): @@ -96,12 +95,7 @@ class DaikinClimate(ClimateDevice): self._list = { ATTR_HVAC_MODE: list(HA_STATE_TO_DAIKIN), ATTR_FAN_MODE: self._api.device.fan_rate, - ATTR_SWING_MODE: list( - map( - str.title, - appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE]), - ) - ), + ATTR_SWING_MODE: self._api.device.swing_modes, } self._supported_features = SUPPORT_TARGET_TEMPERATURE @@ -156,7 +150,7 @@ class DaikinClimate(ClimateDevice): @property def unique_id(self): """Return a unique ID.""" - return self._api.mac + return self._api.device.mac @property def temperature_unit(self): diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 35f21ef3e0d..cd5be5cef29 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -1,16 +1,17 @@ """Config flow for the Daikin platform.""" import asyncio import logging +from uuid import uuid4 -from aiohttp import ClientError +from aiohttp import ClientError, web_exceptions from async_timeout import timeout -from pydaikin.appliance import Appliance +from pydaikin.daikin_base import Appliance import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD -from .const import KEY_IP, KEY_MAC, TIMEOUT +from .const import CONF_KEY, CONF_UUID, KEY_IP, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -22,43 +23,92 @@ class FlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - async def _create_entry(self, host, mac): + def __init__(self): + """Initialize the Daikin config flow.""" + self.host = None + + @property + def schema(self): + """Return current schema.""" + return vol.Schema( + { + vol.Required(CONF_HOST, default=self.host): str, + vol.Optional(CONF_KEY): str, + vol.Optional(CONF_PASSWORD): str, + } + ) + + async def _create_entry(self, host, mac, key=None, uuid=None, password=None): """Register new entry.""" # Check if mac already is registered - for entry in self._async_current_entries(): - if entry.data[KEY_MAC] == mac: - return self.async_abort(reason="already_configured") + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=host, data={CONF_HOST: host, KEY_MAC: mac}) + return self.async_create_entry( + title=host, + data={ + CONF_HOST: host, + KEY_MAC: mac, + CONF_KEY: key, + CONF_UUID: uuid, + CONF_PASSWORD: password, + }, + ) - async def _create_device(self, host): + async def _create_device(self, host, key=None, password=None): """Create device.""" + # BRP07Cxx devices needs uuid together with key + if key: + uuid = str(uuid4()) + else: + uuid = None + key = None + + if not password: + password = None try: - device = Appliance( - host, self.hass.helpers.aiohttp_client.async_get_clientsession() - ) with timeout(TIMEOUT): - await device.init() + device = await Appliance.factory( + host, + self.hass.helpers.aiohttp_client.async_get_clientsession(), + key=key, + uuid=uuid, + password=password, + ) except asyncio.TimeoutError: - return self.async_abort(reason="device_timeout") + return self.async_show_form( + step_id="user", + data_schema=self.schema, + errors={"base": "device_timeout"}, + ) + except web_exceptions.HTTPForbidden: + return self.async_show_form( + step_id="user", data_schema=self.schema, errors={"base": "forbidden"}, + ) except ClientError: _LOGGER.exception("ClientError") - return self.async_abort(reason="device_fail") + return self.async_show_form( + step_id="user", data_schema=self.schema, errors={"base": "device_fail"}, + ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") - return self.async_abort(reason="device_fail") + return self.async_show_form( + step_id="user", data_schema=self.schema, errors={"base": "device_fail"}, + ) - mac = device.values.get("mac") - return await self._create_entry(host, mac) + mac = device.mac + return await self._create_entry(host, mac, key, uuid, password) async def async_step_user(self, user_input=None): """User initiated config flow.""" if user_input is None: - return self.async_show_form( - step_id="user", data_schema=vol.Schema({vol.Required(CONF_HOST): str}) - ) - return await self._create_device(user_input[CONF_HOST]) + return self.async_show_form(step_id="user", data_schema=self.schema) + return await self._create_device( + user_input[CONF_HOST], + user_input.get(CONF_KEY), + user_input.get(CONF_PASSWORD), + ) async def async_step_import(self, user_input): """Import a config entry.""" @@ -67,7 +117,10 @@ class FlowHandler(config_entries.ConfigFlow): return await self.async_step_user() return await self._create_device(host) - async def async_step_discovery(self, user_input): + async def async_step_discovery(self, discovery_info): """Initialize step from discovery.""" - _LOGGER.info("Discovered device: %s", user_input) - return await self._create_entry(user_input[KEY_IP], user_input[KEY_MAC]) + _LOGGER.debug("Discovered device: %s", discovery_info) + await self.async_set_unique_id(discovery_info[KEY_MAC]) + self._abort_if_unique_id_configured() + self.host = discovery_info[KEY_IP] + return await self.async_step_user() diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 15ae5321bf3..30d34b898d3 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -1,28 +1,67 @@ """Constants for Daikin.""" -from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + POWER_KILO_WATT, + TEMP_CELSIUS, +) ATTR_TARGET_TEMPERATURE = "target_temperature" ATTR_INSIDE_TEMPERATURE = "inside_temperature" ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_TOTAL_POWER = "total_power" +ATTR_COOL_ENERGY = "cool_energy" +ATTR_HEAT_ENERGY = "heat_energy" ATTR_STATE_ON = "on" ATTR_STATE_OFF = "off" SENSOR_TYPE_TEMPERATURE = "temperature" +SENSOR_TYPE_POWER = "power" +SENSOR_TYPE_ENERGY = "energy" SENSOR_TYPES = { ATTR_INSIDE_TEMPERATURE: { CONF_NAME: "Inside Temperature", - CONF_ICON: "mdi:thermometer", CONF_TYPE: SENSOR_TYPE_TEMPERATURE, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ATTR_OUTSIDE_TEMPERATURE: { CONF_NAME: "Outside Temperature", - CONF_ICON: "mdi:thermometer", CONF_TYPE: SENSOR_TYPE_TEMPERATURE, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ATTR_TOTAL_POWER: { + CONF_NAME: "Total Power Consumption", + CONF_TYPE: SENSOR_TYPE_POWER, + CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, + CONF_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + }, + ATTR_COOL_ENERGY: { + CONF_NAME: "Cool Energy Consumption", + CONF_TYPE: SENSOR_TYPE_ENERGY, + CONF_ICON: "mdi:snowflake", + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + }, + ATTR_HEAT_ENERGY: { + CONF_NAME: "Heat Energy Consumption", + CONF_TYPE: SENSOR_TYPE_ENERGY, + CONF_ICON: "mdi:fire", + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, } +CONF_KEY = "key" +CONF_UUID = "uuid" + KEY_MAC = "mac" KEY_IP = "ip" diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index c501fa7c120..9732962de5a 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==1.6.3"], + "requirements": ["pydaikin==2.0.2"], "codeowners": ["@fredrike"], "quality_scale": "platinum" } diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index d0d8e4b0fda..eeaa162c2d8 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -1,11 +1,27 @@ """Support for Daikin AC sensors.""" import logging -from homeassistant.const import CONF_ICON, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, +) from homeassistant.helpers.entity import Entity -from . import DOMAIN as DAIKIN_DOMAIN -from .const import ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, SENSOR_TYPES +from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi +from .const import ( + ATTR_COOL_ENERGY, + ATTR_HEAT_ENERGY, + ATTR_INSIDE_TEMPERATURE, + ATTR_OUTSIDE_TEMPERATURE, + ATTR_TOTAL_POWER, + SENSOR_TYPE_ENERGY, + SENSOR_TYPE_POWER, + SENSOR_TYPE_TEMPERATURE, + SENSOR_TYPES, +) _LOGGER = logging.getLogger(__name__) @@ -24,13 +40,27 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors = [ATTR_INSIDE_TEMPERATURE] if daikin_api.device.support_outside_temperature: sensors.append(ATTR_OUTSIDE_TEMPERATURE) - async_add_entities([DaikinClimateSensor(daikin_api, sensor) for sensor in sensors]) + if daikin_api.device.support_energy_consumption: + sensors.append(ATTR_TOTAL_POWER) + sensors.append(ATTR_COOL_ENERGY) + sensors.append(ATTR_HEAT_ENERGY) + async_add_entities([DaikinSensor.factory(daikin_api, sensor) for sensor in sensors]) -class DaikinClimateSensor(Entity): +class DaikinSensor(Entity): """Representation of a Sensor.""" - def __init__(self, api, monitored_state) -> None: + @staticmethod + def factory(api: DaikinApi, monitored_state: str): + """Initialize any DaikinSensor.""" + cls = { + SENSOR_TYPE_TEMPERATURE: DaikinClimateSensor, + SENSOR_TYPE_POWER: DaikinPowerSensor, + SENSOR_TYPE_ENERGY: DaikinPowerSensor, + }[SENSOR_TYPES[monitored_state][CONF_TYPE]] + return cls(api, monitored_state) + + def __init__(self, api: DaikinApi, monitored_state: str) -> None: """Initialize the sensor.""" self._api = api self._sensor = SENSOR_TYPES[monitored_state] @@ -40,12 +70,7 @@ class DaikinClimateSensor(Entity): @property def unique_id(self): """Return a unique ID.""" - return f"{self._api.mac}-{self._device_attribute}" - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._sensor[CONF_ICON] + return f"{self._api.device.mac}-{self._device_attribute}" @property def name(self): @@ -55,16 +80,22 @@ class DaikinClimateSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self._device_attribute == ATTR_INSIDE_TEMPERATURE: - return self._api.device.inside_temperature - if self._device_attribute == ATTR_OUTSIDE_TEMPERATURE: - return self._api.device.outside_temperature - return None + raise NotImplementedError + + @property + def device_class(self): + """Return the class of this device.""" + return self._sensor.get(CONF_DEVICE_CLASS) + + @property + def icon(self): + """Return the icon of this device.""" + return self._sensor.get(CONF_ICON) @property def unit_of_measurement(self): """Return the unit of measurement.""" - return TEMP_CELSIUS + return self._sensor[CONF_UNIT_OF_MEASUREMENT] async def async_update(self): """Retrieve latest state.""" @@ -74,3 +105,31 @@ class DaikinClimateSensor(Entity): def device_info(self): """Return a device description for device registry.""" return self._api.device_info + + +class DaikinClimateSensor(DaikinSensor): + """Representation of a Climate Sensor.""" + + @property + def state(self): + """Return the internal state of the sensor.""" + if self._device_attribute == ATTR_INSIDE_TEMPERATURE: + return self._api.device.inside_temperature + if self._device_attribute == ATTR_OUTSIDE_TEMPERATURE: + return self._api.device.outside_temperature + return None + + +class DaikinPowerSensor(DaikinSensor): + """Representation of a power/energy consumption sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self._device_attribute == ATTR_TOTAL_POWER: + return round(self._api.device.current_total_power_consumption, 3) + if self._device_attribute == ATTR_COOL_ENERGY: + return round(self._api.device.last_hour_cool_power_consumption, 3) + if self._device_attribute == ATTR_HEAT_ENERGY: + return round(self._api.device.last_hour_heat_power_consumption, 3) + return None diff --git a/homeassistant/components/daikin/strings.json b/homeassistant/components/daikin/strings.json index 1e82d285eee..c60163577a6 100644 --- a/homeassistant/components/daikin/strings.json +++ b/homeassistant/components/daikin/strings.json @@ -3,14 +3,21 @@ "step": { "user": { "title": "Configure Daikin AC", - "description": "Enter IP address of your Daikin AC.", - "data": { "host": "Host" } + "description": "Enter IP address of your Daikin AC.\n\nNote that [%key:common::config_flow::data::api_key%] and [%key:common::config_flow::data::password%] are used by BRP072Cxx and SKYFi devices respectively.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "key": "[%key:common::config_flow::data::api_key%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "abort": { - "device_timeout": "Timeout connecting to the device.", - "device_fail": "Unexpected error creating device.", - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "device_fail": "[%key:common::config_flow::error::unknown%]", + "forbidden": "[%key:common::config_flow::error::invalid_auth%]", + "device_timeout": "[%key:common::config_flow::error::cannot_connect%]" } } } diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index b7131c29bdd..0dae8848d39 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -43,7 +43,7 @@ class DaikinZoneSwitch(ToggleEntity): @property def unique_id(self): """Return a unique ID.""" - return f"{self._api.mac}-zone{self._zone_id}" + return f"{self._api.device.mac}-zone{self._zone_id}" @property def icon(self): diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 35d2dafd338..9497a57ea49 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -5,10 +5,17 @@ "device_fail": "S'ha produ\u00eft un error inesperat al crear el dispositiu.", "device_timeout": "S'ha acabat el temps d'espera en la connexi\u00f3 amb el dispositiu." }, + "error": { + "device_fail": "Error inesperat", + "device_timeout": "No s'ha pogut connectar", + "forbidden": "Autenticaci\u00f3 inv\u00e0lida" + }, "step": { "user": { "data": { - "host": "Amfitri\u00f3" + "host": "Amfitri\u00f3", + "key": "Clau API", + "password": "Contrasenya" }, "description": "Introdueix l'adre\u00e7a IP del teu Daikin AC.", "title": "Configuraci\u00f3 de Daikin AC" diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index ac7df0863bf..f3f55ea2ecb 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -8,7 +8,9 @@ "step": { "user": { "data": { - "host": "Host" + "host": "Host", + "key": "Authentifizierungsschl\u00fcssel (wird nur von BRP072C / Zena-Ger\u00e4ten verwendet)", + "password": "Ger\u00e4tekennwort (wird nur von SKYFi-Ger\u00e4ten verwendet)" }, "description": "Gib die IP-Adresse deiner Daikin AC ein.", "title": "Daikin AC konfigurieren" diff --git a/homeassistant/components/daikin/translations/en.json b/homeassistant/components/daikin/translations/en.json index f66f360d096..30ba908e04f 100644 --- a/homeassistant/components/daikin/translations/en.json +++ b/homeassistant/components/daikin/translations/en.json @@ -5,10 +5,17 @@ "device_fail": "Unexpected error creating device.", "device_timeout": "Timeout connecting to the device." }, + "error": { + "device_fail": "Unexpected error", + "device_timeout": "Failed to connect", + "forbidden": "Invalid authentication" + }, "step": { "user": { "data": { - "host": "Host" + "host": "Host", + "key": "API Key", + "password": "Password" }, "description": "Enter IP address of your Daikin AC.", "title": "Configure Daikin AC" diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index b774ac67ed3..ae3d205e15d 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -5,10 +5,17 @@ "device_fail": "Error inesperado al crear el dispositivo.", "device_timeout": "Tiempo de espera agotado al conectar con el dispositivo." }, + "error": { + "device_fail": "Error inesperado", + "device_timeout": "Error al conectar", + "forbidden": "Autenticaci\u00f3n no v\u00e1lida" + }, "step": { "user": { "data": { - "host": "Host" + "host": "Host", + "key": "Clave de autenticaci\u00f3n (s\u00f3lo utilizada por dispositivos BRP072C/Zena)", + "password": "Contrase\u00f1a del dispositivo (s\u00f3lo utilizada por dispositivos SKYFi)" }, "description": "Introduce la IP de tu aire acondicionado Daikin", "title": "Configurar aire acondicionado Daikin" diff --git a/homeassistant/components/daikin/translations/fi.json b/homeassistant/components/daikin/translations/fi.json new file mode 100644 index 00000000000..ed772ef7c06 --- /dev/null +++ b/homeassistant/components/daikin/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Palvelin" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index b15a9fae262..3a3f08ae1f3 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -8,7 +8,9 @@ "step": { "user": { "data": { - "host": "H\u00f4te" + "host": "H\u00f4te", + "key": "Cl\u00e9 d'authentification (utilis\u00e9e uniquement par les appareils BRP072C/Zena)", + "password": "Mot de passe de l'appareil (utilis\u00e9 uniquement par les appareils SKYFi)" }, "description": "Entrez l'adresse IP de votre Daikin AC.", "title": "Configurer Daikin AC" diff --git a/homeassistant/components/daikin/translations/he.json b/homeassistant/components/daikin/translations/he.json new file mode 100644 index 00000000000..bde111561a2 --- /dev/null +++ b/homeassistant/components/daikin/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "device_fail": "\u05d0\u05d9\u05e8\u05d0\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4", + "device_timeout": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "forbidden": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "key": "\u05de\u05e4\u05ea\u05d7 API", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index 72d5acd97a8..634d500ef1e 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -5,10 +5,17 @@ "device_fail": "Errore inatteso durante la creazione del dispositivo.", "device_timeout": "Tempo scaduto per la connessione al dispositivo." }, + "error": { + "device_fail": "Errore imprevisto", + "device_timeout": "Impossibile connettersi", + "forbidden": "Autenticazione non valida" + }, "step": { "user": { "data": { - "host": "Host" + "host": "Host", + "key": "Chiave API", + "password": "Password" }, "description": "Inserisci l'indirizzo IP del tuo Daikin AC.", "title": "Configura Daikin AC" diff --git a/homeassistant/components/daikin/translations/ko.json b/homeassistant/components/daikin/translations/ko.json index 129da9b87d0..2ce657c8e06 100644 --- a/homeassistant/components/daikin/translations/ko.json +++ b/homeassistant/components/daikin/translations/ko.json @@ -5,13 +5,20 @@ "device_fail": "\uae30\uae30\ub97c \uad6c\uc131\ud558\ub294 \ub3c4\uc911 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "device_timeout": "\uae30\uae30 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4." }, + "error": { + "device_fail": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "device_timeout": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "forbidden": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { "data": { - "host": "\ud638\uc2a4\ud2b8" + "host": "\ud638\uc2a4\ud2b8", + "key": "API \ud0a4", + "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131" + "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/daikin/translations/lb.json b/homeassistant/components/daikin/translations/lb.json index 4fab38c9115..25039f5af80 100644 --- a/homeassistant/components/daikin/translations/lb.json +++ b/homeassistant/components/daikin/translations/lb.json @@ -5,10 +5,17 @@ "device_fail": "Onerwaarte Feeler beim erstelle vum Apparat.", "device_timeout": "Z\u00e4it Iwwerschreidung beim verbannen mam Apparat." }, + "error": { + "device_fail": "Onerwaarte Feeler", + "device_timeout": "Feeler beim verbannen", + "forbidden": "Ong\u00eblteg Authentifikatioun" + }, "step": { "user": { "data": { - "host": "Apparat" + "host": "Apparat", + "key": "Authentifikatiouns Schl\u00ebssel (n\u00ebmme vu BRP072C/Zena Apparater benotzt)", + "password": "Passwuert vum Apparat (n\u00ebmme vun SKYFi Apparater benotzt)" }, "description": "Gitt d'IP Adresse vum Daikin AC an:", "title": "Daikin AC konfigur\u00e9ieren" diff --git a/homeassistant/components/daikin/translations/no.json b/homeassistant/components/daikin/translations/no.json index 42d13cf6844..2bfddeb9973 100644 --- a/homeassistant/components/daikin/translations/no.json +++ b/homeassistant/components/daikin/translations/no.json @@ -8,9 +8,11 @@ "step": { "user": { "data": { - "host": "Vert" + "host": "Vert", + "key": "Godkjenningsn\u00f8kkel (brukes bare av BRP072C/Zena enheter)", + "password": "Enhetspassord (brukes bare av SKYFi-enheter)" }, - "description": "Angi IP-adressen til din Daikin AC.", + "description": "Fyll inn IP-adressen til din Daikin AC.", "title": "Konfigurer Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json index 2e2f65bc008..76999ad718c 100644 --- a/homeassistant/components/daikin/translations/pl.json +++ b/homeassistant/components/daikin/translations/pl.json @@ -1,14 +1,21 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", "device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem." }, + "error": { + "device_fail": "[%key_id:common::config_flow::error::unknown%]", + "device_timeout": "[%key_id:common::config_flow::error::cannot_connect%]", + "forbidden": "[%key_id:common::config_flow::error::invalid_auth%]" + }, "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP" + "host": "[%key_id:common::config_flow::data::host%]", + "key": "Klucz uwierzytelniania (u\u017cywany tylko przez urz\u0105dzenia BRP072C/Zena)", + "password": "Has\u0142o urz\u0105dzenia (u\u017cywane tylko przez urz\u0105dzenia SKYFi)" }, "description": "Wprowad\u017a adres IP Daikin AC.", "title": "Konfiguracja Daikin AC" diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json index a7b57fcb757..780945c0537 100644 --- a/homeassistant/components/daikin/translations/ru.json +++ b/homeassistant/components/daikin/translations/ru.json @@ -5,10 +5,17 @@ "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, + "error": { + "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "device_timeout": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "forbidden": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, "step": { "user": { "data": { - "host": "\u0425\u043e\u0441\u0442" + "host": "\u0425\u043e\u0441\u0442", + "key": "\u041a\u043b\u044e\u0447 API", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Daikin AC.", "title": "Daikin AC" diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index 0825d6ed396..db785feab0b 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -8,7 +8,8 @@ "step": { "user": { "data": { - "host": "V\u00e4rddatorn" + "host": "V\u00e4rddatorn", + "password": "Enhetsl\u00f6senord (anv\u00e4nds endast av SKYFi-enheter)" }, "description": "Ange IP-adressen f\u00f6r din Daikin AC.", "title": "Konfigurera Daikin AC" diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index bab54118687..c5f367684d5 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -5,10 +5,17 @@ "device_fail": "\u5275\u5efa\u8a2d\u5099\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", "device_timeout": "\u9023\u7dda\u81f3\u8a2d\u5099\u903e\u6642\u3002" }, + "error": { + "device_fail": "\u672a\u9810\u671f\u932f\u8aa4", + "device_timeout": "\u9023\u7dda\u5931\u6557", + "forbidden": "\u9a57\u8b49\u78bc\u7121\u6548" + }, "step": { "user": { "data": { - "host": "\u4e3b\u6a5f\u7aef" + "host": "\u4e3b\u6a5f\u7aef", + "key": "API \u5bc6\u9470", + "password": "\u5bc6\u78bc" }, "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abf IP \u4f4d\u5740\u3002", "title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf" diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index caac12c1b20..7f6876a709b 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the for Danfoss Air HRV binary sensors.""" from pydanfossair.commands import ReadCommand -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DOMAIN as DANFOSS_AIR_DOMAIN @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class DanfossAirBinarySensor(BinarySensorDevice): +class DanfossAirBinarySensor(BinarySensorEntity): """Representation of a Danfoss Air binary sensor.""" def __init__(self, data, name, sensor_type, device_class): diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index 96e363951c8..dc4c79ed10f 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -3,7 +3,7 @@ import logging from pydanfossair.commands import ReadCommand, UpdateCommand -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DOMAIN as DANFOSS_AIR_DOMAIN @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class DanfossAir(SwitchDevice): +class DanfossAir(SwitchEntity): """Representation of a Danfoss Air HRV Switch.""" def __init__(self, data, name, state_command, on_command, off_command): diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index e3742231e1e..5bd7d972482 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -6,7 +6,7 @@ import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -610,6 +610,14 @@ class DarkSkySensor(Entity): return SENSOR_TYPES[self.type][6] + @property + def device_class(self): + """Device class of the entity.""" + if SENSOR_TYPES[self.type][1] == TEMP_CELSIUS: + return DEVICE_CLASS_TEMPERATURE + + return None + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index d16722525f9..95fa223c697 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,7 +1,7 @@ """Support for deCONZ binary sensors.""" from pydeconz.sensor import Presence, Vibration -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): +class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" @callback diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 7b0f44807ec..424693505ca 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,7 +1,7 @@ """Support for deCONZ climate devices.""" from pydeconz.sensor import Thermostat -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, @@ -58,7 +58,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_climate(gateway.api.sensors.values()) -class DeconzThermostat(DeconzDevice, ClimateDevice): +class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" @property diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 7db3477c3bb..e01cfdbe5f8 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -5,7 +5,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -46,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_cover(gateway.api.lights.values()) -class DeconzCover(DeconzDevice, CoverDevice): +class DeconzCover(DeconzDevice, CoverEntity): """Representation of a deCONZ cover.""" def __init__(self, device, gateway): diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index a146552f14e..b0486a99dc8 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -300,6 +300,50 @@ AQARA_SQUARE_SWITCH_WXKG11LM_2016 = { (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1006}, } +AQARA_OPPLE_2_BUTTONS_MODEL = "lumi.remote.b286opcn01" +AQARA_OPPLE_2_BUTTONS = { + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 1001}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 1002}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 1003}, + (CONF_DOUBLE_PRESS, CONF_TURN_OFF): {CONF_EVENT: 1004}, + (CONF_TRIPLE_PRESS, CONF_TURN_OFF): {CONF_EVENT: 1005}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 2001}, + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 2002}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 2003}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 2004}, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 2005}, +} + +AQARA_OPPLE_4_BUTTONS_MODEL = "lumi.remote.b486opcn01" +AQARA_OPPLE_4_BUTTONS = { + **AQARA_OPPLE_2_BUTTONS, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_DOUBLE_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3004}, + (CONF_TRIPLE_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3005}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 4001}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 4002}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 4003}, + (CONF_DOUBLE_PRESS, CONF_DIM_UP): {CONF_EVENT: 4004}, + (CONF_TRIPLE_PRESS, CONF_DIM_UP): {CONF_EVENT: 4005}, +} + +AQARA_OPPLE_6_BUTTONS_MODEL = "lumi.remote.b686opcn01" +AQARA_OPPLE_6_BUTTONS = { + **AQARA_OPPLE_4_BUTTONS, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 5001}, + (CONF_SHORT_RELEASE, CONF_LEFT): {CONF_EVENT: 5002}, + (CONF_LONG_RELEASE, CONF_LEFT): {CONF_EVENT: 5003}, + (CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 5004}, + (CONF_TRIPLE_PRESS, CONF_LEFT): {CONF_EVENT: 5005}, + (CONF_LONG_PRESS, CONF_RIGHT): {CONF_EVENT: 6001}, + (CONF_SHORT_RELEASE, CONF_RIGHT): {CONF_EVENT: 6002}, + (CONF_LONG_RELEASE, CONF_RIGHT): {CONF_EVENT: 6003}, + (CONF_DOUBLE_PRESS, CONF_RIGHT): {CONF_EVENT: 6004}, + (CONF_TRIPLE_PRESS, CONF_RIGHT): {CONF_EVENT: 6005}, +} + REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, @@ -319,6 +363,9 @@ REMOTES = { AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL: AQARA_SQUARE_SWITCH_WXKG11LM_2016, + AQARA_OPPLE_2_BUTTONS_MODEL: AQARA_OPPLE_2_BUTTONS, + AQARA_OPPLE_4_BUTTONS_MODEL: AQARA_OPPLE_4_BUTTONS, + AQARA_OPPLE_6_BUTTONS_MODEL: AQARA_OPPLE_6_BUTTONS, } TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e836f1e4490..48d286266e4 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_group(gateway.api.groups.values()) -class DeconzLight(DeconzDevice, Light): +class DeconzLight(DeconzDevice, LightEntity): """Representation of a deCONZ light.""" def __init__(self, device, gateway): diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 5ff4a303b0c..e33af57099d 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -9,6 +9,6 @@ "manufacturer": "Royal Philips Electronics" } ], - "codeowners": ["@kane610"], + "codeowners": ["@Kane610"], "quality_scale": "platinum" } diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 2042c36c859..cf5acb5cff7 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -9,8 +9,8 @@ }, "manual_input": { "data": { - "host": "Host", - "port": "Port" + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" } }, "link": { @@ -22,7 +22,9 @@ "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?" } }, - "error": { "no_key": "Couldn't get an API key" }, + "error": { + "no_key": "Couldn't get an API key" + }, "abort": { "already_configured": "Bridge is already configured", "already_in_progress": "Config flow for bridge is already in progress.", @@ -98,4 +100,4 @@ "side_6": "Side 6" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 1b51256580a..d7b6b55fbb8 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -1,5 +1,5 @@ """Support for deCONZ switches.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -43,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_switch(gateway.api.lights.values()) -class DeconzPowerPlug(DeconzDevice, SwitchDevice): +class DeconzPowerPlug(DeconzDevice, SwitchEntity): """Representation of a deCONZ power plug.""" @property @@ -62,7 +62,7 @@ class DeconzPowerPlug(DeconzDevice, SwitchDevice): await self._device.async_set_state(data) -class DeconzSiren(DeconzDevice, SwitchDevice): +class DeconzSiren(DeconzDevice, SwitchEntity): """Representation of a deCONZ siren.""" @property diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json index 9d867b0c8e7..208616b7ebe 100644 --- a/homeassistant/components/deconz/translations/es-419.json +++ b/homeassistant/components/deconz/translations/es-419.json @@ -11,6 +11,7 @@ "error": { "no_key": "No se pudo obtener una clave de API" }, + "flow_title": "Puerta de enlace Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", @@ -28,26 +29,86 @@ "host": "Host", "port": "Puerto" } + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar la puerta de enlace deCONZ" + }, + "user": { + "data": { + "host": "Seleccione la puerta de enlace descubierta deCONZ" + }, + "title": "Seleccione la puerta de enlace deCONZ" } } }, "device_automation": { "trigger_subtype": { "both_buttons": "Ambos botones", + "bottom_buttons": "Botones inferiores", "button_1": "Primer bot\u00f3n", "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Aumentar intensidad", "left": "Izquierda", "open": "Abrir", "right": "Derecha", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6", + "top_buttons": "Botones superiores", "turn_off": "Apagar", "turn_on": "Encender" }, "trigger_type": { + "remote_awakened": "Dispositivo despertado", + "remote_button_double_press": "El bot\u00f3n \"{subtype}\" fue presionado 2 veces", + "remote_button_long_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado continuamente", + "remote_button_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 4 veces", + "remote_button_quintuple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 5 veces", "remote_button_rotated": "Bot\u00f3n girado \"{subtype}\"", - "remote_gyro_activated": "Dispositivo agitado" + "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", + "remote_button_short_press": "Se presion\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_short_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_triple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 3 veces", + "remote_double_tap": "Dispositivo \"{subtype}\" doble toque", + "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", + "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_flip_180_degrees": "Dispositivo volteado 180 grados", + "remote_flip_90_degrees": "Dispositivo volteado 90 grados", + "remote_gyro_activated": "Dispositivo agitado", + "remote_moved": "Dispositivo movido con \"{subtype}\" arriba", + "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", + "remote_rotate_from_side_1": "Dispositivo girado de \"lado 1\" a \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \"{subtype}\"", + "remote_turned_clockwise": "Dispositivo girado en sentido de las agujas del reloj", + "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ", + "title": "Opciones de deCONZ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 5ef7c0cc5d9..3299ecbdc55 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "El puente ya esta configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", - "no_bridges": "No se han descubierto puentes deCONZ", - "not_deconz_bridge": "No es un puente deCONZ", + "already_configured": "La pasarela ya est\u00e1 configurada", + "already_in_progress": "La configuraci\u00f3n del flujo para la pasarela ya est\u00e1 en curso.", + "no_bridges": "No se han descubierto pasarelas deCONZ", + "not_deconz_bridge": "No es una pasarela deCONZ", "one_instance_only": "El componente solo admite una instancia de deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, diff --git a/homeassistant/components/deconz/translations/fi.json b/homeassistant/components/deconz/translations/fi.json new file mode 100644 index 00000000000..2a3882a0957 --- /dev/null +++ b/homeassistant/components/deconz/translations/fi.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Silta on jo m\u00e4\u00e4ritetty", + "no_bridges": "deCONZ-siltoja ei l\u00f6ydy" + }, + "error": { + "no_key": "API-avainta ei voitu saada" + }, + "step": { + "init": { + "title": "M\u00e4\u00e4rit\u00e4 deCONZ-yhdysk\u00e4yt\u00e4v\u00e4" + }, + "link": { + "title": "Linkit\u00e4 deCONZiin" + }, + "manual_confirm": { + "data": { + "host": "Palvelin", + "port": "Portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index b7d77ad7b1e..9a1c4120149 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -34,13 +34,15 @@ "data": { "host": "H\u00f4te", "port": "Port" - } + }, + "title": "Configurer la passerelle deCONZ" } } }, "device_automation": { "trigger_subtype": { "both_buttons": "Les deux boutons", + "bottom_buttons": "Boutons du bas", "button_1": "Premier bouton", "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", @@ -57,6 +59,7 @@ "side_4": "Face 4", "side_5": "Face 5", "side_6": "Face 6", + "top_buttons": "Boutons du haut", "turn_off": "\u00c9teint", "turn_on": "Allumer" }, diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index e6e22abc332..ba7d0b271b9 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\ube0c\ub9bf\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", - "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "not_deconz_bridge": "deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_deconz_bridge": "deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4", "updated_instance": "deCONZ \uc778\uc2a4\ud134\uc2a4\ub97c \uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4" }, @@ -18,11 +18,11 @@ "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, "init": { - "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758" + "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758\ud558\uae30" }, "link": { "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30.\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Authenticate app\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", - "title": "deCONZ\uc640 \uc5f0\uacb0" + "title": "deCONZ \uc5f0\uacb0\ud558\uae30" }, "manual_confirm": { "data": { @@ -34,7 +34,14 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" - } + }, + "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uad6c\uc131\ud558\uae30" + }, + "user": { + "data": { + "host": "\ubc1c\uacac\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd" + }, + "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30" } } }, diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index d9d64070c88..8b0caa869f8 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -48,6 +48,7 @@ "device_automation": { "trigger_subtype": { "both_buttons": "Beide knoppen", + "bottom_buttons": "Onderste knoppen", "button_1": "Eerste knop", "button_2": "Tweede knop", "button_3": "Derde knop", @@ -64,6 +65,7 @@ "side_4": "Zijde 4", "side_5": "Zijde 5", "side_6": "Zijde 6", + "top_buttons": "Bovenste knoppen", "turn_off": "Uitschakelen", "turn_on": "Inschakelen" }, diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index c743774c41b..19c6358baf5 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -21,7 +21,7 @@ "title": "Definer deCONZ-gatewayen" }, "link": { - "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", + "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen", "title": "Koble til deCONZ" }, "manual_confirm": { @@ -59,12 +59,12 @@ "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", - "side_1": "Side 1", - "side_2": "Side 2", - "side_3": "Side 3", - "side_4": "Side 4", - "side_5": "Side 5", - "side_6": "Side 6", + "side_1": "", + "side_2": "", + "side_3": "", + "side_4": "", + "side_5": "", + "side_6": "", "top_buttons": "\u00d8verste knappene", "turn_off": "Skru av", "turn_on": "Sl\u00e5 p\u00e5" diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index a9bff098644..6477dfa9445 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -26,14 +26,14 @@ }, "manual_confirm": { "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port" + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]" } }, "manual_input": { "data": { "host": "Nazwa hosta lub adres IP", - "port": "Port" + "port": "[%key_id:common::config_flow::data::port%]" }, "title": "Konfiguracja bramki deCONZ" }, diff --git a/homeassistant/components/deconz/translations/sl.json b/homeassistant/components/deconz/translations/sl.json index 3d7902ef1a0..af23635af80 100644 --- a/homeassistant/components/deconz/translations/sl.json +++ b/homeassistant/components/deconz/translations/sl.json @@ -29,6 +29,19 @@ "host": "Gostitelj", "port": "Vrata" } + }, + "manual_input": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + }, + "title": "Konfigurirajte prehod deCONZ" + }, + "user": { + "data": { + "host": "Izberite odkrit prehod deCONZ" + }, + "title": "Izberite prehod deCONZ" } } }, diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 559581b8ad8..e7e0f5d917f 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -29,6 +29,12 @@ "host": "V\u00e4rd", "port": "Port (standardv\u00e4rde: '80')" } + }, + "manual_input": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } } } }, diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index f4035352e51..5b6015b7c54 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(lights) -class DecoraLight(Light): +class DecoraLight(LightEntity): """Representation of an Decora light.""" def __init__(self, device): diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 9071da9707d..6f716d3a5dc 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout) -class DecoraWifiLight(Light): +class DecoraWifiLight(LightEntity): """Representation of a Decora WiFi switch.""" def __init__(self, switch): diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index d324ac862e3..0b80e172904 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -12,6 +12,7 @@ "map", "mobile_app", "person", + "scene", "script", "ssdp", "sun", diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 0323b68b1b0..d5bb71da67b 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -3,8 +3,8 @@ import datetime from homeassistant.components.manual.alarm_control_panel import ManualAlarm from homeassistant.const import ( + CONF_ARMING_TIME, CONF_DELAY_TIME, - CONF_PENDING_TIME, CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -28,18 +28,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= False, { STATE_ALARM_ARMED_AWAY: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_HOME: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_NIGHT: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_DISARMED: { @@ -47,12 +47,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_CUSTOM_BYPASS: { + CONF_ARMING_TIME: datetime.timedelta(seconds=5), CONF_DELAY_TIME: datetime.timedelta(seconds=0), - CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_TRIGGERED: { - CONF_PENDING_TIME: datetime.timedelta(seconds=5) + CONF_ARMING_TIME: datetime.timedelta(seconds=5) }, }, ) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 0f6dfa9f357..04d8e72f9a8 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,5 +1,5 @@ """Demo platform that has two fake binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DOMAIN @@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoBinarySensor(BinarySensorDevice): +class DemoBinarySensor(BinarySensorEntity): """representation of a Demo binary sensor.""" def __init__(self, unique_id, name, state, device_class): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 0edcf618ba6..fd5615c82bd 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,7 +1,7 @@ """Demo platform that offers a fake climate device.""" import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -97,7 +97,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoClimate(ClimateDevice): +class DemoClimate(ClimateEntity): """Representation of a demo climate device.""" def __init__( @@ -134,8 +134,6 @@ class DemoClimate(ClimateDevice): self._support_flags = self._support_flags | SUPPORT_TARGET_HUMIDITY if swing_mode is not None: self._support_flags = self._support_flags | SUPPORT_SWING_MODE - if hvac_action is not None: - self._support_flags = self._support_flags if aux is not None: self._support_flags = self._support_flags | SUPPORT_AUX_HEAT if HVAC_MODE_HEAT_COOL in hvac_modes or HVAC_MODE_AUTO in hvac_modes: diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index ab95cc978b3..e65d6e59ece 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -4,7 +4,7 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, - CoverDevice, + CoverEntity, ) from homeassistant.core import callback from homeassistant.helpers.event import async_track_utc_time_change @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoCover(CoverDevice): +class DemoCover(CoverEntity): """Representation of a demo cover.""" def __init__( diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 966ba51cacb..4b75e817153 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -37,12 +37,12 @@ class DemoFan(FanEntity): self.hass = hass self._supported_features = supported_features self._speed = STATE_OFF - self.oscillating = None + self._oscillating = None self._direction = None self._name = name if supported_features & SUPPORT_OSCILLATE: - self.oscillating = False + self._oscillating = False if supported_features & SUPPORT_DIRECTION: self._direction = "forward" @@ -89,7 +89,7 @@ class DemoFan(FanEntity): def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" - self.oscillating = oscillating + self._oscillating = oscillating self.schedule_update_ha_state() @property @@ -97,6 +97,11 @@ class DemoFan(FanEntity): """Fan direction.""" return self._direction + @property + def oscillating(self) -> bool: + """Oscillating.""" + return self._oscillating + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index e6747fee2df..11b6a4812e8 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from . import DOMAIN @@ -57,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoLight(Light): +class DemoLight(LightEntity): """Representation of a demo light.""" def __init__( diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 5074741d83d..63f2d218957 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,5 +1,5 @@ """Demo lock platform that has two fake locks.""" -from homeassistant.components.lock import SUPPORT_OPEN, LockDevice +from homeassistant.components.lock import SUPPORT_OPEN, LockEntity from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoLock(LockDevice): +class DemoLock(LockEntity): """Representation of a Demo lock.""" def __init__(self, name, state, openable=False): diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 7a8f4eb8fbe..9cfb5582acc 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,5 +1,5 @@ """Demo implementation of the media player.""" -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -93,7 +93,7 @@ NETFLIX_PLAYER_SUPPORT = ( ) -class AbstractDemoPlayer(MediaPlayerDevice): +class AbstractDemoPlayer(MediaPlayerEntity): """A demo media players.""" # We only implement the methods that we support diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 70e0d3c8b6e..9d12621fef1 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -1,5 +1,5 @@ """Demo platform that has two fake remotes.""" -from homeassistant.components.remote import RemoteDevice +from homeassistant.components.remote import RemoteEntity from homeassistant.const import DEVICE_DEFAULT_NAME @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): ) -class DemoRemote(RemoteDevice): +class DemoRemote(RemoteEntity): """Representation of a demo remote.""" def __init__(self, name, state, icon): diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 5050b2283b4..cdbeb142677 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,5 +1,5 @@ """Demo platform that has two fake switches.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME from . import DOMAIN @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoSwitch(SwitchDevice): +class DemoSwitch(SwitchEntity): """Representation of a demo switch.""" def __init__(self, unique_id, name, state, icon, assumed, device_class=None): diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index a4176a43085..70d7f550d0b 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -11,7 +11,7 @@ "data": { "multi": "Selecci\u00f3 m\u00faltiple", "select": "Selecciona una opci\u00f3", - "string": "Valor de cadena (string)" + "string": "Valor de l'String" } } } diff --git a/homeassistant/components/demo/translations/es-419.json b/homeassistant/components/demo/translations/es-419.json index a9abb4aacd9..8057621520a 100644 --- a/homeassistant/components/demo/translations/es-419.json +++ b/homeassistant/components/demo/translations/es-419.json @@ -1,3 +1,20 @@ { + "options": { + "step": { + "options_1": { + "data": { + "bool": "Booleano opcional", + "int": "Entrada num\u00e9rica" + } + }, + "options_2": { + "data": { + "multi": "Selecci\u00f3n m\u00faltiple", + "select": "Seleccione una opci\u00f3n", + "string": "Valor de cadena" + } + } + } + }, "title": "Demo" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index ed813c9e505..e85f5b067a0 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -16,5 +16,5 @@ } } }, - "title": "Demo" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 0bdf3ed48f1..a5d85aa9bd6 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -21,8 +21,8 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - StateVacuumDevice, - VacuumDevice, + StateVacuumEntity, + VacuumEntity, ) _LOGGER = logging.getLogger(__name__) @@ -95,7 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class DemoVacuum(VacuumDevice): +class DemoVacuum(VacuumEntity): """Representation of a demo vacuum.""" def __init__(self, name, supported_features): @@ -254,7 +254,7 @@ class DemoVacuum(VacuumDevice): self.schedule_update_ha_state() -class StateDemoVacuum(StateVacuumDevice): +class StateDemoVacuum(StateVacuumEntity): """Representation of a demo vacuum supporting states.""" def __init__(self, name): diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index f9aca141245..0b96bbf75f8 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -3,7 +3,7 @@ from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoWaterHeater(WaterHeaterDevice): +class DemoWaterHeater(WaterHeaterEntity): """Representation of a demo water_heater device.""" def __init__( diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 1fbd4885f43..9f451ab3025 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -4,7 +4,7 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -86,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([denon]) -class DenonDevice(MediaPlayerDevice): +class DenonDevice(MediaPlayerEntity): """Representation of a Denon device.""" def __init__(self, name, host): diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 7713354b14a..524e728588b 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -6,7 +6,7 @@ import logging import denonavr import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, @@ -159,7 +159,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(receivers) -class DenonDevice(MediaPlayerDevice): +class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" def __init__(self, receiver): diff --git a/homeassistant/components/device_tracker/translations/it.json b/homeassistant/components/device_tracker/translations/it.json index bcb97536310..1a59a123072 100644 --- a/homeassistant/components/device_tracker/translations/it.json +++ b/homeassistant/components/device_tracker/translations/it.json @@ -7,7 +7,7 @@ }, "state": { "_": { - "home": "A casa", + "home": "In casa", "not_home": "Fuori casa" } }, diff --git a/homeassistant/components/device_tracker/translations/no.json b/homeassistant/components/device_tracker/translations/no.json index 8073a7f5871..56ef663e6c6 100644 --- a/homeassistant/components/device_tracker/translations/no.json +++ b/homeassistant/components/device_tracker/translations/no.json @@ -5,5 +5,11 @@ "is_not_home": "{entity_name} er ikke hjemme" } }, + "state": { + "_": { + "home": "Hjemme", + "not_home": "Borte" + } + }, "title": "Enhetssporing" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py new file mode 100644 index 00000000000..cfe1549f3c4 --- /dev/null +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -0,0 +1,82 @@ +"""The devolo_home_control integration.""" +from functools import partial + +from devolo_home_control_api.homecontrol import HomeControl +from devolo_home_control_api.mydevolo import Mydevolo + +from homeassistant.components import switch as ha_switch +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.typing import HomeAssistantType + +from .const import CONF_HOMECONTROL, CONF_MYDEVOLO, DOMAIN, PLATFORMS + +SUPPORTED_PLATFORMS = [ha_switch.DOMAIN] + + +async def async_setup(hass, config): + """Get all devices and add them to hass.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up the devolo account from a config entry.""" + conf = entry.data + hass.data.setdefault(DOMAIN, {}) + try: + mydevolo = Mydevolo.get_instance() + except SyntaxError: + mydevolo = Mydevolo() + + mydevolo.user = conf[CONF_USERNAME] + mydevolo.password = conf[CONF_PASSWORD] + mydevolo.url = conf[CONF_MYDEVOLO] + mydevolo.mprm = conf[CONF_HOMECONTROL] + + credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) + + if not credentials_valid: + return False + + if await hass.async_add_executor_job(mydevolo.maintenance): + raise ConfigEntryNotReady + + gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids) + gateway_id = gateway_ids[0] + mprm_url = mydevolo.mprm + + try: + hass.data[DOMAIN]["homecontrol"] = await hass.async_add_executor_job( + partial(HomeControl, gateway_id=gateway_id, url=mprm_url) + ) + except ConnectionError: + raise ConfigEntryNotReady + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + def shutdown(event): + hass.data[DOMAIN]["homecontrol"].websocket_disconnect( + f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}" + ) + + # Listen when EVENT_HOMEASSISTANT_STOP is fired + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload = await hass.config_entries.async_forward_entry_unload( + config_entry, "switch" + ) + + await hass.async_add_executor_job( + hass.data[DOMAIN]["homecontrol"].websocket_disconnect + ) + del hass.data[DOMAIN]["homecontrol"] + return unload diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py new file mode 100644 index 00000000000..d104bdde275 --- /dev/null +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure the devolo home control integration.""" +import logging + +from devolo_home_control_api.mydevolo import Mydevolo +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import ( # pylint:disable=unused-import + CONF_HOMECONTROL, + CONF_MYDEVOLO, + DEFAULT_MPRM, + DEFAULT_MYDEVOLO, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a devolo HomeControl config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + def __init__(self): + """Initialize devolo Home Control flow.""" + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str, + vol.Required(CONF_HOMECONTROL, default=DEFAULT_MPRM): str, + } + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_form(user_input) + user = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + try: + mydevolo = Mydevolo.get_instance() + except SyntaxError: + mydevolo = Mydevolo() + mydevolo.user = user + mydevolo.password = password + mydevolo.url = user_input[CONF_MYDEVOLO] + mydevolo.mprm = user_input[CONF_HOMECONTROL] + credentials_valid = await self.hass.async_add_executor_job( + mydevolo.credentials_valid + ) + if not credentials_valid: + return self._show_form({"base": "invalid_credentials"}) + _LOGGER.debug("Credentials valid") + gateway_ids = await self.hass.async_add_executor_job(mydevolo.get_gateway_ids) + await self.async_set_unique_id(gateway_ids[0]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="devolo Home Control", + data={ + CONF_PASSWORD: password, + CONF_USERNAME: user, + CONF_MYDEVOLO: mydevolo.url, + CONF_HOMECONTROL: mydevolo.mprm, + }, + ) + + @callback + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py new file mode 100644 index 00000000000..0d5bb9a3356 --- /dev/null +++ b/homeassistant/components/devolo_home_control/const.py @@ -0,0 +1,8 @@ +"""Constants for the devolo_home_control integration.""" + +DOMAIN = "devolo_home_control" +DEFAULT_MYDEVOLO = "https://www.mydevolo.com" +DEFAULT_MPRM = "https://homecontrol.mydevolo.com" +PLATFORMS = ["switch"] +CONF_MYDEVOLO = "mydevolo_url" +CONF_HOMECONTROL = "home_control_url" diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json new file mode 100644 index 00000000000..e3a4e2f8720 --- /dev/null +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "devolo_home_control", + "name": "devolo_home_control", + "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", + "requirements": ["devolo-home-control-api==0.10.0"], + "config_flow": true, + "codeowners": [ + "@2Fake", + "@Shutgun"], + "quality_scale": "silver" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json new file mode 100644 index 00000000000..620ca421854 --- /dev/null +++ b/homeassistant/components/devolo_home_control/strings.json @@ -0,0 +1,22 @@ +{ + "title": "devolo Home Control", + "config": { + "abort": { + "already_configured": "This Home Control Central Unit is already in use." + }, + "error": { + "invalid_credentials": "Incorrect user name and/or password." + }, + "step": { + "user": { + "data": { + "username": "E-Mail-Address / devolo ID", + "password": "[%key:common::config_flow::data::password%]", + "mydevolo_url": "mydevolo URL", + "home_control_url": "Home Control URL" + }, + "title": "devolo Home Control" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py new file mode 100644 index 00000000000..e5210262268 --- /dev/null +++ b/homeassistant/components/devolo_home_control/switch.py @@ -0,0 +1,152 @@ +"""Platform for light integration.""" +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Get all devices and setup the switch devices via config entry.""" + devices = hass.data[DOMAIN]["homecontrol"].binary_switch_devices + + entities = [] + for device in devices: + for binary_switch in device.binary_switch_property: + entities.append( + DevoloSwitch( + homecontrol=hass.data[DOMAIN]["homecontrol"], + device_instance=device, + element_uid=binary_switch, + ) + ) + async_add_entities(entities) + + +class DevoloSwitch(SwitchDevice): + """Representation of an Awesome Light.""" + + def __init__(self, homecontrol, device_instance, element_uid): + """Initialize an devolo Switch.""" + self._device_instance = device_instance + + # Create the unique ID + self._unique_id = element_uid + + self._homecontrol = homecontrol + self._name = self._device_instance.itemName + self._available = self._device_instance.is_online() + + # Get the brand and model information + self._brand = self._device_instance.brand + self._model = self._device_instance.name + + self._binary_switch_property = self._device_instance.binary_switch_property.get( + self._unique_id + ) + self._is_on = self._binary_switch_property.state + + if hasattr(self._device_instance, "consumption_property"): + self._consumption = self._device_instance.consumption_property.get( + self._unique_id.replace("BinarySwitch", "Meter") + ).current + else: + self._consumption = None + + self.subscriber = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.subscriber = Subscriber(self._device_instance.itemName, callback=self.sync) + self._homecontrol.publisher.register( + self._device_instance.uid, self.subscriber, self.sync + ) + + @property + def unique_id(self): + """Return the unique ID of the switch.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._device_instance.uid)}, + "name": self.name, + "manufacturer": self._brand, + "model": self._model, + } + + @property + def device_id(self): + """Return the ID of this switch.""" + return self._unique_id + + @property + def name(self): + """Return the display name of this switch.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return the state.""" + return self._is_on + + @property + def current_power_w(self): + """Return the current consumption.""" + return self._consumption + + @property + def available(self): + """Return the online state.""" + return self._available + + def turn_on(self, **kwargs): + """Switch on the device.""" + self._is_on = True + self._binary_switch_property.set(state=True) + + def turn_off(self, **kwargs): + """Switch off the device.""" + self._is_on = False + self._binary_switch_property.set(state=False) + + def sync(self, message=None): + """Update the binary switch state and consumption.""" + if message[0].startswith("devolo.BinarySwitch"): + self._is_on = self._device_instance.binary_switch_property[message[0]].state + elif message[0].startswith("devolo.Meter"): + self._consumption = self._device_instance.consumption_property[ + message[0] + ].current + elif message[0].startswith("hdm"): + self._available = self._device_instance.is_online() + else: + _LOGGER.debug("No valid message received: %s", message) + self.schedule_update_ha_state() + + +class Subscriber: + """Subscriber class for the publisher in mprm websocket class.""" + + def __init__(self, name, callback): + """Initiate the device.""" + self.name = name + self.callback = callback + + def update(self, message): + """Trigger hass to update the device.""" + _LOGGER.debug('%s got message "%s"', self.name, message) + self.callback(message) diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json new file mode 100644 index 00000000000..920e9a0780e --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Aquesta Central Unit de Home Control ja est\u00e0 en \u00fas." + }, + "error": { + "invalid_credentials": "Nom d'usuari i/o contrasenya incorrectes." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "URL de Home Control", + "Mydevolo_URL": "URL de mydevolo", + "home_control_url": "URL de Home Control", + "mydevolo_url": "URL de mydevolo", + "password": "Contrasenya", + "username": "Correu electr\u00f2nic / ID de devolo" + }, + "description": "Configura el teu Home Control de devolo.", + "title": "Home Control devolo" + } + } + }, + "title": "Home Control devolo" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json new file mode 100644 index 00000000000..b8a464d6222 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Diese Home Control Zentral wird bereits verwendet." + }, + "error": { + "invalid_credentials": "Falscher Benutzername und/oder Passwort." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Passwort", + "username": "E-Mail-Adresse / devolo ID" + }, + "description": "Richten Sie Ihr devolo Home Control ein.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json new file mode 100644 index 00000000000..3f1ab4d45e3 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "This Home Control Central Unit is already in use." + }, + "error": { + "invalid_credentials": "Incorrect user name and/or password." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Password", + "username": "E-Mail-Address / devolo ID" + }, + "description": "Set up your devolo Home Control.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/es-419.json b/homeassistant/components/devolo_home_control/translations/es-419.json new file mode 100644 index 00000000000..eab3a5fddfe --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/es-419.json @@ -0,0 +1,3 @@ +{ + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json new file mode 100644 index 00000000000..108b36b187e --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Esta Unidad Central de Home Control ya est\u00e1 en uso." + }, + "error": { + "invalid_credentials": "Nombre de usuario y/o contrase\u00f1a incorrectos." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "URL de Home Control", + "Mydevolo_URL": "URL de mydevolo", + "home_control_url": "URL de Home Control", + "mydevolo_url": "URL de mydevolo", + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico / ID de devolo" + }, + "description": "Configurar el devolo Home Control.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/fi.json b/homeassistant/components/devolo_home_control/translations/fi.json new file mode 100644 index 00000000000..c2957e7c2b5 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/fi.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Salasana" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json new file mode 100644 index 00000000000..34d4c7524e4 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_credentials": "Nom d''utilisateur et/ou mot de passe incorrect." + }, + "step": { + "user": { + "data": { + "mydevolo_url": "URL mydevolo", + "password": "Mot de passe", + "username": "Adresse e-mail / devolo ID" + }, + "description": "Installez votre devolo Home Control.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json new file mode 100644 index 00000000000..0042ba21e31 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_credentials": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5/\u05d0\u05d5 \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd." + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json new file mode 100644 index 00000000000..14997ea7b7a --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Questo Home Control Central \u00e8 gi\u00e0 in uso." + }, + "error": { + "invalid_credentials": "Nome utente e/o password non corretti." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "URL di Home Control", + "Mydevolo_URL": "URL di mydevolo", + "home_control_url": "URL di Home Control", + "mydevolo_url": "URL di mydevolo", + "password": "Password", + "username": "Indirizzo e-mail / devolo ID" + }, + "description": "Configura devolo Home Control.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/ko.json b/homeassistant/components/devolo_home_control/translations/ko.json new file mode 100644 index 00000000000..cf6ea82353a --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 Home Control Central \uc720\ub2db\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + }, + "error": { + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c / devolo ID" + }, + "description": "devolo Home Control \uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/lb.json b/homeassistant/components/devolo_home_control/translations/lb.json new file mode 100644 index 00000000000..0f95319fb2c --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebs Home Control Zentrale ass scho konfigur\u00e9iert." + }, + "error": { + "invalid_credentials": "Ong\u00ebltege Benotzernumm an/oder Passwuert" + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "devolo Home Control ariichten.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json new file mode 100644 index 00000000000..10b71ff595a --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_credentials": "Verkeerde gebruikersnaam en/of wachtwoord." + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail adres / devolo ID" + }, + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json new file mode 100644 index 00000000000..950c7b6736f --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Home Control Central er allerede konfigurert." + }, + "error": { + "invalid_credentials": "Ugyldig brukernavn og/eller passord" + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "", + "Mydevolo_URL": "", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Sett opp din devolo Home Control.", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/pl.json b/homeassistant/components/devolo_home_control/translations/pl.json new file mode 100644 index 00000000000..ef8beb5ff01 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ta jednostka Home Control Central jest ju\u017c w u\u017cyciu." + }, + "error": { + "invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika i/lub has\u0142o." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "URL Home Control", + "Mydevolo_URL": "URL mydevolo", + "home_control_url": "URL Home Control", + "mydevolo_url": "URL mydevolo", + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" + }, + "description": "Konfiguracja devolo Home Control", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json new file mode 100644 index 00000000000..12760361ff8 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e\u0442 Home Control Central Unit \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 devolo Home Control.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json new file mode 100644 index 00000000000..a932043a52f --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "invalid_credentials": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord" + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "L\u00f6senord", + "username": "E-postadress / devolo-ID" + }, + "description": "St\u00e4ll in din devolo Home Control.", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json new file mode 100644 index 00000000000..581420cba08 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64 Home Control Central \u5df2\u7d93\u65bc\u4f7f\u7528\u4e2d\u3002" + }, + "error": { + "invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u53ca/\u6216\u5bc6\u78bc\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "Home_Control_URL": "Home Control URL", + "Mydevolo_URL": "mydevolo URL", + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "\u5bc6\u78bc", + "username": "E-Mail \u4f4d\u5740 / devolo ID" + }, + "description": "\u8a2d\u5b9a devolo Home Control\u3002", + "title": "devolo Home Control" + } + } + }, + "title": "devolo Home Control" +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/ko.json b/homeassistant/components/dialogflow/translations/ko.json index ef49094efdb..f9ee10ceee7 100644 --- a/homeassistant/components/dialogflow/translations/ko.json +++ b/homeassistant/components/dialogflow/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow \uc6f9 \ud6c5]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "description": "Dialogflow \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Dialogflow Webhook \uc124\uc815" + "title": "Dialogflow \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index c3515177535..d076dae9210 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv @@ -50,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class DigitalOceanBinarySensor(BinarySensorDevice): +class DigitalOceanBinarySensor(BinarySensorEntity): """Representation of a Digital Ocean droplet sensor.""" def __init__(self, do, droplet_id): diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index 9b9b8157bce..811b844e35c 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -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 ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv @@ -50,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class DigitalOceanSwitch(SwitchDevice): +class DigitalOceanSwitch(SwitchEntity): """Representation of a Digital Ocean droplet switch.""" def __init__(self, do, droplet_id): diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 268ec581c00..7448b9fbcf3 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -5,7 +5,7 @@ import logging import dlipower 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_HOST, CONF_NAME, @@ -72,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(outlets) -class DINRelay(SwitchDevice): +class DINRelay(SwitchEntity): """Representation of an individual DIN III relay port.""" def __init__(self, controller_name, parent_device, outlet): diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index ec39734573b..205503fe17f 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -4,7 +4,7 @@ from typing import Callable, List from directv import DIRECTV -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -77,7 +77,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerDevice): +class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): """Representation of a DirecTV receiver on the network.""" def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: @@ -141,7 +141,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerDevice): return self._address - # MediaPlayerDevice properties and methods + # MediaPlayerEntity properties and methods @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index e8137ace711..9665b0aea17 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Iterable, List from directv import DIRECTV, DIRECTVError -from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteDevice +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -36,7 +36,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class DIRECTVRemote(DIRECTVEntity, RemoteDevice): +class DIRECTVRemote(DIRECTVEntity, RemoteEntity): """Device that sends commands to a DirecTV receiver.""" def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json index 24b97165513..7a07185978b 100644 --- a/homeassistant/components/directv/strings.json +++ b/homeassistant/components/directv/strings.json @@ -7,12 +7,16 @@ "description": "Do you want to set up {name}?" }, "user": { - "data": { "host": "Host or IP address" } + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } }, - "error": { "cannot_connect": "Failed to connect, please try again" }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { - "already_configured": "DirecTV receiver is already configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "unknown": "Unexpected error" } } diff --git a/homeassistant/components/directv/translations/ca.json b/homeassistant/components/directv/translations/ca.json index 98156c3c701..d906fa2b6fc 100644 --- a/homeassistant/components/directv/translations/ca.json +++ b/homeassistant/components/directv/translations/ca.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "El receptor DirecTV ja est\u00e0 configurat", + "already_configured": "El dispositiu ja est\u00e0 configurat", "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar" + "cannot_connect": "No s'ha pogut connectar" }, "flow_title": "DirecTV: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Amfitri\u00f3 o adre\u00e7a IP" + "host": "Amfitri\u00f3" }, "title": "Connexi\u00f3 amb el receptor DirecTV" } diff --git a/homeassistant/components/directv/translations/en.json b/homeassistant/components/directv/translations/en.json index e271497ae34..8df2c1aec66 100644 --- a/homeassistant/components/directv/translations/en.json +++ b/homeassistant/components/directv/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "DirecTV receiver is already configured", + "already_configured": "Device is already configured", "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again" + "cannot_connect": "Failed to connect" }, "flow_title": "DirecTV: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Host or IP address" + "host": "Host" }, "title": "Connect to the DirecTV receiver" } diff --git a/homeassistant/components/directv/translations/es-419.json b/homeassistant/components/directv/translations/es-419.json new file mode 100644 index 00000000000..6db50cd6b5a --- /dev/null +++ b/homeassistant/components/directv/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El receptor de DirecTV ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "\u00bfDesea configurar {name}?", + "title": "Conectarse al receptor DirecTV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "title": "Conectarse al receptor DirecTV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index a69cc9c32dd..cb47b845de6 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "El receptor DirecTV ya est\u00e1 configurado", + "already_configured": "Dispositivo ya configurado", "unknown": "Error inesperado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo." + "cannot_connect": "Error al conectar" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/translations/fr.json b/homeassistant/components/directv/translations/fr.json index d165476de66..0ff3dd4edb4 100644 --- a/homeassistant/components/directv/translations/fr.json +++ b/homeassistant/components/directv/translations/fr.json @@ -10,6 +10,10 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { + "data": { + "one": "Vide", + "other": "Vide" + }, "description": "Voulez-vous configurer {name} ?", "title": "Connectez-vous au r\u00e9cepteur DirecTV" }, diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json index 3c740c8966c..d75f10976d9 100644 --- a/homeassistant/components/directv/translations/it.json +++ b/homeassistant/components/directv/translations/it.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Il ricevitore DirecTV \u00e8 gi\u00e0 configurato", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "unknown": "Errore imprevisto" }, "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare" + "cannot_connect": "Impossibile connettersi" }, "flow_title": "DirecTV: {name}", "step": { @@ -19,7 +19,7 @@ }, "user": { "data": { - "host": "Host o indirizzo IP" + "host": "Host" }, "title": "Collegamento al ricevitore DirecTV" } diff --git a/homeassistant/components/directv/translations/ko.json b/homeassistant/components/directv/translations/ko.json index 8c3bbb94a8d..4a1fbd3dbbe 100644 --- a/homeassistant/components/directv/translations/ko.json +++ b/homeassistant/components/directv/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "DirecTV \ub9ac\uc2dc\ubc84\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "DirecTV: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + "host": "\ud638\uc2a4\ud2b8" }, "title": "DirecTV \ub9ac\uc2dc\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" } diff --git a/homeassistant/components/directv/translations/nl.json b/homeassistant/components/directv/translations/nl.json new file mode 100644 index 00000000000..b6635311064 --- /dev/null +++ b/homeassistant/components/directv/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "DirecTV-ontvanger is al geconfigureerd", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Wilt u {name} instellen?", + "title": "Maak verbinding met de DirecTV-ontvanger" + }, + "user": { + "data": { + "host": "Host- of IP-adres" + }, + "title": "Maak verbinding met de DirecTV-ontvanger" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json index 23010c90c1f..f5f657c75ab 100644 --- a/homeassistant/components/directv/translations/pl.json +++ b/homeassistant/components/directv/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." @@ -21,7 +21,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP" + "host": "[%key_id:common::config_flow::data::host%]" }, "title": "Po\u0142\u0105czenie z odbiornikiem DirecTV" } diff --git a/homeassistant/components/directv/translations/ru.json b/homeassistant/components/directv/translations/ru.json index a4538099480..5c48aa0be58 100644 --- a/homeassistant/components/directv/translations/ru.json +++ b/homeassistant/components/directv/translations/ru.json @@ -5,7 +5,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "flow_title": "DirecTV: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + "host": "\u0425\u043e\u0441\u0442" }, "title": "DirecTV" } diff --git a/homeassistant/components/directv/translations/sv.json b/homeassistant/components/directv/translations/sv.json new file mode 100644 index 00000000000..c42c03d9944 --- /dev/null +++ b/homeassistant/components/directv/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + }, + "step": { + "ssdp_confirm": { + "description": "Do vill du konfigurera {name}?" + }, + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index 6546dafc133..7668adb6c24 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "DirectTV \u63a5\u6536\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "flow_title": "DirecTV\uff1a{name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + "host": "\u4e3b\u6a5f\u7aef" }, "title": "\u9023\u7dda\u81f3 DirecTV \u63a5\u6536\u5668" } diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index fb36a60eecc..11f83d80179 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -65,6 +65,7 @@ class DiscordNotificationService(BaseNotificationService): images.append(image) else: _LOGGER.warning("Image not found: %s", image) + # pylint: disable=unused-variable @discord_bot.event async def on_ready(): diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 227995db971..afcf8cc341d 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -32,7 +32,6 @@ SERVICE_FREEBOX = "freebox" SERVICE_HASS_IOS_APP = "hass_ios" SERVICE_HASSIO = "hassio" SERVICE_HEOS = "heos" -SERVICE_IGD = "igd" SERVICE_KONNECTED = "konnected" SERVICE_MOBILE_APP = "hass_mobile_app" SERVICE_NETGEAR = "netgear_router" @@ -48,7 +47,6 @@ SERVICE_XIAOMI_GW = "xiaomi_gw" CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", - SERVICE_IGD: "upnp", } SERVICE_HANDLERS = { @@ -73,7 +71,6 @@ SERVICE_HANDLERS = { "openhome": ("media_player", "openhome"), "bose_soundtouch": ("media_player", "soundtouch"), "bluesound": ("media_player", "bluesound"), - "songpal": ("media_player", "songpal"), "kodi": ("media_player", "kodi"), "volumio": ("media_player", "volumio"), "lg_smart_device": ("media_player", "lg_soundbar"), @@ -93,6 +90,7 @@ MIGRATED_SERVICE_HANDLERS = [ "ikea_tradfri", "philips_hue", "sonos", + "songpal", SERVICE_WEMO, ] diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 1cc0e16f30f..c173c879ad1 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -6,7 +6,7 @@ import urllib from pyW215.pyW215 import SmartPlug import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SmartPlugSwitch(hass, data, name)], True) -class SmartPlugSwitch(SwitchDevice): +class SmartPlugSwitch(SwitchEntity): """Representation of a D-Link Smart Plug switch.""" def __init__(self, hass, data, name): diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 1e3ba840d6f..75d88d59c32 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -11,7 +11,7 @@ from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequest from async_upnp_client.profiles.dlna import DeviceState, DmrDevice import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, @@ -194,7 +194,7 @@ async def async_setup_platform( async_add_entities([device], True) -class DlnaDmrDevice(MediaPlayerDevice): +class DlnaDmrDevice(MediaPlayerEntity): """Representation of a DLNA DMR device.""" def __init__(self, dmr_device, name=None): diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 3fa20f2eea4..f363f46d2d7 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,6 +2,6 @@ "domain": "doods", "name": "DOODS - Distributed Outside Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==7.1.1"], + "requirements": ["pydoods==1.0.2", "pillow==7.1.2"], "codeowners": [] } diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index b70b0a3061c..048cd87c3aa 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import get_url from homeassistant.util import dt as dt_util, slugify from .const import CONF_EVENTS, DOMAIN, DOOR_STATION, DOOR_STATION_INFO, PLATFORMS @@ -252,7 +253,7 @@ class ConfiguredDoorBird: def register_events(self, hass): """Register events on device.""" # Get the URL of this server - hass_url = hass.config.api.base_url + hass_url = get_url(hass) # Override url if another is specified in the configuration if self.custom_url is not None: diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index e27083d2e09..6f1f866053e 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -2,7 +2,9 @@ "options": { "step": { "init": { - "data": { "events": "Comma separated list of events." }, + "data": { + "events": "Comma separated list of events." + }, "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" } } @@ -12,10 +14,10 @@ "user": { "title": "Connect to the DoorBird", "data": { - "password": "Password", - "host": "Host (IP Address)", + "password": "[%key:common::config_flow::data::password%]", + "host": "[%key:common::config_flow::data::host%]", "name": "Device Name", - "username": "Username" + "username": "[%key:common::config_flow::data::username%]" } } }, @@ -26,9 +28,9 @@ }, "flow_title": "DoorBird {name} ({host})", "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", - "cannot_connect": "Failed to connect, please try again" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index 9f292803b8b..1e4cb81a5eb 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -2,7 +2,7 @@ import datetime import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity import homeassistant.util.dt as dt_util from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO @@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class DoorBirdSwitch(DoorBirdEntity, SwitchDevice): +class DoorBirdSwitch(DoorBirdEntity, SwitchEntity): """A relay in a DoorBird device.""" def __init__(self, doorstation, doorstation_info, relay): diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json index 9bd469ee4e1..efc7e92a78d 100644 --- a/homeassistant/components/doorbird/translations/ca.json +++ b/homeassistant/components/doorbird/translations/ca.json @@ -6,9 +6,9 @@ "not_doorbird_device": "Aquest dispositiu no \u00e9s DoorBird" }, "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key::common::config_flow::error::invalid_auth%]", + "unknown": "[%key::common::config_flow::error::unknown%]" }, "flow_title": "DoorBird {name} ({host})", "step": { @@ -16,8 +16,8 @@ "data": { "host": "Amfitri\u00f3 (adre\u00e7a IP)", "name": "Nom del dispositiu", - "password": "Contrasenya", - "username": "Nom d'usuari" + "password": "[%key::common::config_flow::data::password%]", + "username": "[%key::common::config_flow::data::username%]" }, "title": "Connexi\u00f3 amb DoorBird" } @@ -29,7 +29,7 @@ "data": { "events": "Llista d'esdeveniments separats per comes." }, - "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d\u2019introduir-los, utilitzeu l\u2019aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" + "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d'introduir-los, utilitzeu l'aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" } } } diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index ad9d99e555d..0544374ea0f 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -6,8 +6,8 @@ "not_doorbird_device": "Dieses Ger\u00e4t ist kein DoorBird" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", - "invalid_auth": "Ung\u00fcltige Authentifizierung", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifikation", "unknown": "Unerwarteter Fehler" }, "flow_title": "DoorBird {name} ({host})", diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index 7e3ba803771..adf3127ffa0 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -6,7 +6,7 @@ "not_doorbird_device": "This device is not a DoorBird" }, "error": { - "cannot_connect": "Failed to connect, please try again", + "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Host (IP Address)", + "host": "Host", "name": "Device Name", "password": "Password", "username": "Username" diff --git a/homeassistant/components/doorbird/translations/es-419.json b/homeassistant/components/doorbird/translations/es-419.json new file mode 100644 index 00000000000..6b893b93014 --- /dev/null +++ b/homeassistant/components/doorbird/translations/es-419.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Este DoorBird ya est\u00e1 configurado", + "link_local_address": "Las direcciones locales de enlace no son compatibles", + "not_doorbird_device": "Este dispositivo no es un DoorBird" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente" + }, + "flow_title": "DoorBird {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host (direcci\u00f3n IP)", + "name": "Nombre del dispositivo" + }, + "title": "Conectar con DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Lista de eventos separados por comas." + }, + "description": "Agregue un nombre de evento separado por comas para cada evento que desee rastrear. Despu\u00e9s de ingresarlos aqu\u00ed, use la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulte la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 70717aef6ad..b9c77b9ae91 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -6,7 +6,7 @@ "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { - "cannot_connect": "No se pudo conectar, por favor int\u00e9ntalo de nuevo", + "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index c208e035d5a..58dbde2c58d 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -2,20 +2,22 @@ "config": { "abort": { "already_configured": "Ce DoorBird est d\u00e9j\u00e0 configur\u00e9", + "link_local_address": "Les adresses locales ne sont pas prises en charge", "not_doorbird_device": "Cet appareil n'est pas un DoorBird" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "invalid_auth": "Authentification non valide", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { "host": "H\u00f4te (adresse IP)", "name": "Nom de l'appareil", "password": "Mot de passe", - "username": "Nom d'utilisateur" + "username": "Identifiant" }, "title": "Connectez-vous au DoorBird" } diff --git a/homeassistant/components/doorbird/translations/he.json b/homeassistant/components/doorbird/translations/he.json new file mode 100644 index 00000000000..f08cbbdff11 --- /dev/null +++ b/homeassistant/components/doorbird/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json new file mode 100644 index 00000000000..dee4ed9ee0f --- /dev/null +++ b/homeassistant/components/doorbird/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index c08c666d6f8..6bfbdbf8401 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -6,7 +6,7 @@ "not_doorbird_device": "Questo dispositivo non \u00e8 un DoorBird" }, "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/doorbird/translations/ko.json b/homeassistant/components/doorbird/translations/ko.json index 72632afed89..852d325403f 100644 --- a/homeassistant/components/doorbird/translations/ko.json +++ b/homeassistant/components/doorbird/translations/ko.json @@ -6,7 +6,7 @@ "not_doorbird_device": "\uc774 \uae30\uae30\ub294 DoorBird \uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 (IP \uc8fc\uc18c)", + "host": "\ud638\uc2a4\ud2b8", "name": "\uae30\uae30 \uc774\ub984", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json new file mode 100644 index 00000000000..85180df8b4a --- /dev/null +++ b/homeassistant/components/doorbird/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Deze DoorBird is al geconfigureerd", + "link_local_address": "Link-lokale adressen worden niet ondersteund", + "not_doorbird_device": "Dit apparaat is geen DoorBird" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + }, + "flow_title": "DoorBird {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host (IP-adres)", + "name": "Apparaatnaam" + }, + "title": "Maak verbinding met de DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Door komma's gescheiden lijst met gebeurtenissen." + }, + "description": "Voeg een door komma's gescheiden evenementnaam toe voor elk evenement dat u wilt volgen. Nadat je ze hier hebt ingevoerd, gebruik je de DoorBird-app om ze toe te wijzen aan een specifiek evenement. Zie de documentatie op https://www.home-assistant.io/integrations/doorbird/#events. Voorbeeld: iemand_drukte_knop, beweging" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/no.json b/homeassistant/components/doorbird/translations/no.json index 158b783406a..f7d126b1bc7 100644 --- a/homeassistant/components/doorbird/translations/no.json +++ b/homeassistant/components/doorbird/translations/no.json @@ -6,7 +6,6 @@ "not_doorbird_device": "Denne enheten er ikke en DoorBird" }, "error": { - "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index 61511297468..8cd30868567 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -7,17 +7,17 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa urz\u0105dzenia", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Po\u0142\u0105czenie z DoorBird" } diff --git a/homeassistant/components/doorbird/translations/pt.json b/homeassistant/components/doorbird/translations/pt.json index 6515658d6a7..343bf7fe5a5 100644 --- a/homeassistant/components/doorbird/translations/pt.json +++ b/homeassistant/components/doorbird/translations/pt.json @@ -1,15 +1,9 @@ { "config": { - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" - }, "step": { "user": { "data": { - "name": "Nome do dispositivo", - "password": "Palavra-passe", - "username": "Nome de Utilizador" + "name": "Nome do dispositivo" } } } diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index c6a638f2530..bfe0968c847 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -6,7 +6,7 @@ "not_doorbird_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 DoorBird." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json new file mode 100644 index 00000000000..5adf23a0ef2 --- /dev/null +++ b/homeassistant/components/doorbird/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd (IP-adress)", + "name": "Enhetsnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index b751d1bcf83..281a6e54ac2 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -6,7 +6,7 @@ "not_doorbird_device": "\u6b64\u8a2d\u5099\u4e26\u975e DoorBird" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "\u4e3b\u6a5f\uff08IP \u4f4d\u5740\uff09", + "host": "\u4e3b\u6a5f\u7aef", "name": "\u8a2d\u5099\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index bb32bff2a15..7ef0171dd6c 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -2,7 +2,7 @@ from pdunehd import DuneHDPlayer import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([DuneHDPlayerEntity(DuneHDPlayer(host), name, sources)], True) -class DuneHDPlayerEntity(MediaPlayerDevice): +class DuneHDPlayerEntity(MediaPlayerEntity): """Implementation of the Dune HD player.""" def __init__(self, player, name, sources): diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 2f2ed8f2fa2..78281f56f0f 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -117,7 +117,7 @@ AREA_DATA_SCHEMA = vol.Schema( vol.All( { vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_TEMPLATE): cv.string, + vol.Optional(CONF_TEMPLATE): vol.In(DEFAULT_TEMPLATES), vol.Optional(CONF_FADE): vol.Coerce(float), vol.Optional(CONF_NO_DEFAULT): cv.boolean, vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA, diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 09cf8e25a10..522061a85aa 100644 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -1,21 +1,16 @@ """Code to handle a Dynalite bridge.""" -from typing import TYPE_CHECKING, Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Optional -from dynalite_devices_lib.dynalite_devices import DynaliteDevices +from dynalite_devices_lib.dynalite_devices import DynaliteBaseDevice, DynaliteDevices from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import CONF_ALL, ENTITY_PLATFORMS, LOGGER +from .const import ENTITY_PLATFORMS, LOGGER from .convert_config import convert_config -if TYPE_CHECKING: # pragma: no cover - from dynalite_devices_lib.dynalite_devices import ( # pylint: disable=ungrouped-imports - DynaliteBaseDevice, - ) - class DynaliteBridge: """Manages a single Dynalite bridge.""" @@ -45,7 +40,7 @@ class DynaliteBridge: LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config) self.dynalite_devices.configure(convert_config(config)) - def update_signal(self, device: "DynaliteBaseDevice" = None) -> str: + def update_signal(self, device: Optional[DynaliteBaseDevice] = None) -> str: """Create signal to use to trigger entity update.""" if device: signal = f"dynalite-update-{self.host}-{device.unique_id}" @@ -54,9 +49,9 @@ class DynaliteBridge: return signal @callback - def update_device(self, device: "DynaliteBaseDevice") -> None: + def update_device(self, device: Optional[DynaliteBaseDevice] = None) -> None: """Call when a device or all devices should be updated.""" - if device == CONF_ALL: + if not device: # This is used to signal connection or disconnection, so all devices may become available or not. log_string = ( "Connected" if self.dynalite_devices.connected else "Disconnected" @@ -73,7 +68,7 @@ class DynaliteBridge: if platform in self.waiting_devices: self.async_add_devices[platform](self.waiting_devices[platform]) - def add_devices_when_registered(self, devices: List["DynaliteBaseDevice"]) -> None: + def add_devices_when_registered(self, devices: List[DynaliteBaseDevice]) -> None: """Add the devices to HA if the add devices callback was registered, otherwise queue until it is.""" for platform in ENTITY_PLATFORMS: platform_devices = [ diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index ade167e1b3e..82d66dba7ba 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -14,7 +14,6 @@ CONF_ACTIVE = "active" ACTIVE_INIT = "init" ACTIVE_OFF = "off" ACTIVE_ON = "on" -CONF_ALL = "ALL" CONF_AREA = "area" CONF_AUTO_DISCOVER = "autodiscover" CONF_BRIDGES = "bridges" diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index 03ece744d41..3cc9372eb0b 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -32,47 +32,128 @@ from .const import ( CONF_TIME_COVER, ) -CONF_MAP = { - CONF_ACTIVE: dyn_const.CONF_ACTIVE, - ACTIVE_INIT: dyn_const.CONF_ACTIVE_INIT, - ACTIVE_OFF: dyn_const.CONF_ACTIVE_OFF, - ACTIVE_ON: dyn_const.CONF_ACTIVE_ON, - CONF_AREA: dyn_const.CONF_AREA, - CONF_AUTO_DISCOVER: dyn_const.CONF_AUTO_DISCOVER, - CONF_CHANNEL: dyn_const.CONF_CHANNEL, - CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, - CONF_TYPE: dyn_const.CONF_CHANNEL_TYPE, - CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, - CONF_DEFAULT: dyn_const.CONF_DEFAULT, - CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, - CONF_DURATION: dyn_const.CONF_DURATION, - CONF_FADE: dyn_const.CONF_FADE, - CONF_HOST: dyn_const.CONF_HOST, - CONF_NAME: dyn_const.CONF_NAME, - CONF_NO_DEFAULT: dyn_const.CONF_NO_DEFAULT, - CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, - CONF_POLL_TIMER: dyn_const.CONF_POLL_TIMER, - CONF_PORT: dyn_const.CONF_PORT, - CONF_PRESET: dyn_const.CONF_PRESET, +ACTIVE_MAP = { + ACTIVE_INIT: dyn_const.ACTIVE_INIT, + False: dyn_const.ACTIVE_OFF, + ACTIVE_OFF: dyn_const.ACTIVE_OFF, + ACTIVE_ON: dyn_const.ACTIVE_ON, + True: dyn_const.ACTIVE_ON, +} + +TEMPLATE_MAP = { CONF_ROOM: dyn_const.CONF_ROOM, - CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, - CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, - CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, - CONF_TEMPLATE: dyn_const.CONF_TEMPLATE, - CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, CONF_TIME_COVER: dyn_const.CONF_TIME_COVER, } +def convert_with_map(config, conf_map): + """Create the initial converted map with just the basic key:value pairs updated.""" + result = {} + for conf in conf_map: + if conf in config: + result[conf_map[conf]] = config[conf] + return result + + +def convert_channel(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for a channel.""" + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_FADE: dyn_const.CONF_FADE, + CONF_TYPE: dyn_const.CONF_CHANNEL_TYPE, + } + return convert_with_map(config, my_map) + + +def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for a preset.""" + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_FADE: dyn_const.CONF_FADE, + } + return convert_with_map(config, my_map) + + +def convert_area(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for an area.""" + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_FADE: dyn_const.CONF_FADE, + CONF_NO_DEFAULT: dyn_const.CONF_NO_DEFAULT, + CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, + CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, + CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, + CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, + CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, + CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, + CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, + CONF_DURATION: dyn_const.CONF_DURATION, + CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, + } + result = convert_with_map(config, my_map) + if CONF_CHANNEL in config: + result[dyn_const.CONF_CHANNEL] = { + channel: convert_channel(channel_conf) + for (channel, channel_conf) in config[CONF_CHANNEL].items() + } + if CONF_PRESET in config: + result[dyn_const.CONF_PRESET] = { + preset: convert_preset(preset_conf) + for (preset, preset_conf) in config[CONF_PRESET].items() + } + if CONF_TEMPLATE in config: + result[dyn_const.CONF_TEMPLATE] = TEMPLATE_MAP[config[CONF_TEMPLATE]] + return result + + +def convert_default(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for the platform defaults.""" + return convert_with_map(config, {CONF_FADE: dyn_const.CONF_FADE}) + + +def convert_template(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for a template.""" + my_map = { + CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, + CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, + CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, + CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, + CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, + CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, + CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, + CONF_DURATION: dyn_const.CONF_DURATION, + CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, + } + return convert_with_map(config, my_map) + + def convert_config(config: Dict[str, Any]) -> Dict[str, Any]: """Convert a config dict by replacing component consts with library consts.""" - result = {} - for (key, value) in config.items(): - if isinstance(value, dict): - new_value = convert_config(value) - elif isinstance(value, str): - new_value = CONF_MAP.get(value, value) - else: - new_value = value - result[CONF_MAP.get(key, key)] = new_value + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_HOST: dyn_const.CONF_HOST, + CONF_PORT: dyn_const.CONF_PORT, + CONF_AUTO_DISCOVER: dyn_const.CONF_AUTO_DISCOVER, + CONF_POLL_TIMER: dyn_const.CONF_POLL_TIMER, + } + result = convert_with_map(config, my_map) + if CONF_AREA in config: + result[dyn_const.CONF_AREA] = { + area: convert_area(area_conf) + for (area, area_conf) in config[CONF_AREA].items() + } + if CONF_DEFAULT in config: + result[dyn_const.CONF_DEFAULT] = convert_default(config[CONF_DEFAULT]) + if CONF_ACTIVE in config: + result[dyn_const.CONF_ACTIVE] = ACTIVE_MAP[config[CONF_ACTIVE]] + if CONF_PRESET in config: + result[dyn_const.CONF_PRESET] = { + preset: convert_preset(preset_conf) + for (preset, preset_conf) in config[CONF_PRESET].items() + } + if CONF_TEMPLATE in config: + result[dyn_const.CONF_TEMPLATE] = { + TEMPLATE_MAP[template]: convert_template(template_conf) + for (template, template_conf) in config[CONF_TEMPLATE].items() + } return result diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index dcf16ede58c..e44fd150f38 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -1,11 +1,10 @@ """Support for the Dynalite channels as covers.""" from typing import Callable -from homeassistant.components.cover import DEVICE_CLASSES, CoverDevice +from homeassistant.components.cover import DEVICE_CLASSES, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from .const import DEFAULT_COVER_CLASS from .dynalitebase import DynaliteBase, async_setup_entry_base @@ -25,16 +24,15 @@ async def async_setup_entry( ) -class DynaliteCover(DynaliteBase, CoverDevice): +class DynaliteCover(DynaliteBase, CoverEntity): """Representation of a Dynalite Channel as a Home Assistant Cover.""" @property def device_class(self) -> str: """Return the class of the device.""" dev_cls = self._device.device_class - if dev_cls in DEVICE_CLASSES: - return dev_cls - return DEFAULT_COVER_CLASS + assert dev_cls in DEVICE_CLASSES + return dev_cls @property def current_cover_position(self) -> int: diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 283b1ee2286..5e7069ab50b 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,7 +1,7 @@ """Support for Dynalite channels as lights.""" from typing import Callable -from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +18,7 @@ async def async_setup_entry( ) -class DynaliteLight(DynaliteBase, Light): +class DynaliteLight(DynaliteBase, LightEntity): """Representation of a Dynalite Channel as a Home Assistant Light.""" @property diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 39f72f57b06..581110ba583 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.39"] + "requirements": ["dynalite_devices==0.1.40"] } diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index 45e24d8193a..d106d976d68 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -1,7 +1,7 @@ """Support for the Dynalite channels and presets as switches.""" from typing import Callable -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +18,7 @@ async def async_setup_entry( ) -class DynaliteSwitch(DynaliteBase, SwitchDevice): +class DynaliteSwitch(DynaliteBase, SwitchEntity): """Representation of a Dynalite Channel as a Home Assistant Switch.""" @property diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index f4e23b01622..6b2d7cbe74c 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -5,7 +5,7 @@ from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) -class DysonPureHotCoolLinkDevice(ClimateDevice): +class DysonPureHotCoolLinkDevice(ClimateEntity): """Representation of a Dyson climate fan.""" def __init__(self, device): diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index 6203b65c9db..2306e07072d 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -13,7 +13,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice, + VacuumEntity, ) from homeassistant.helpers.icon import icon_for_battery_level @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class Dyson360EyeDevice(VacuumDevice): +class Dyson360EyeDevice(VacuumEntity): """Dyson 360 Eye robot vacuum device.""" def __init__(self, device): diff --git a/homeassistant/components/ebusd/translations/pl.json b/homeassistant/components/ebusd/translations/pl.json index 0c926a0335c..1d97c3918d2 100644 --- a/homeassistant/components/ebusd/translations/pl.json +++ b/homeassistant/components/ebusd/translations/pl.json @@ -1,6 +1,6 @@ { "state": { - "day": "Dzie\u0144", - "night": "Noc" + "day": "dzie\u0144", + "night": "noc" } } \ No newline at end of file diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index 00bfd7f3e5b..bd3b216d705 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import AVAILABLE_PUMPS, DATA_ECOAL_BOILER @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches, True) -class EcoalSwitch(SwitchDevice): +class EcoalSwitch(SwitchEntity): """Representation of Ecoal switch.""" def __init__(self, ecoal_contr, name, state_attr): diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 2f422007ff4..64c4b07ed1f 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Ecobee binary sensors.""" from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, - BinarySensorDevice, + BinarySensorEntity, ) from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -22,7 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(dev, True) -class EcobeeBinarySensor(BinarySensorDevice): +class EcobeeBinarySensor(BinarySensorEntity): """Representation of an Ecobee sensor.""" def __init__(self, data, sensor_name, sensor_index): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 89f464452f8..a65029fa4cd 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -4,7 +4,7 @@ from typing import Optional import voluptuous as vol -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -249,7 +249,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class Thermostat(ClimateDevice): +class Thermostat(ClimateEntity): """A thermostat class for Ecobee.""" def __init__(self, data, thermostat_index): @@ -288,7 +288,7 @@ class Thermostat(ClimateDevice): else: await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) - if self.hvac_mode is not HVAC_MODE_OFF: + if self.hvac_mode != HVAC_MODE_OFF: self._last_active_hvac_mode = self.hvac_mode @property diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 6e3a5687db1..b80996cb2a3 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -4,7 +4,9 @@ "user": { "title": "ecobee API key", "description": "Please enter the API key obtained from ecobee.com.", - "data": { "api_key": "API Key" } + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } }, "authorize": { "title": "Authorize app on ecobee.com", @@ -19,4 +21,4 @@ "one_instance_only": "This integration currently supports only one ecobee instance." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/ca.json b/homeassistant/components/ecobee/translations/ca.json index 916c700a183..b75006483c7 100644 --- a/homeassistant/components/ecobee/translations/ca.json +++ b/homeassistant/components/ecobee/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "pin_request_failed": "Error al sol\u00b7licitar els PIN d'ecobee; verifica que la clau API \u00e9s correcta.", - "token_request_failed": "Error al sol\u00b7licitar els testimonis d'autenticaci\u00f3 d'ecobee; torna-ho a provar." + "token_request_failed": "Error al sol\u00b7licitar els tokens d'autenticaci\u00f3 d'ecobee; torna-ho a provar." }, "step": { "authorize": { diff --git a/homeassistant/components/ecobee/translations/es-419.json b/homeassistant/components/ecobee/translations/es-419.json index 3e19977f10f..50eab590a1c 100644 --- a/homeassistant/components/ecobee/translations/es-419.json +++ b/homeassistant/components/ecobee/translations/es-419.json @@ -9,7 +9,15 @@ }, "step": { "authorize": { - "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar." + "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar.", + "title": "Autorizar aplicaci\u00f3n en ecobee.com" + }, + "user": { + "data": { + "api_key": "Clave API" + }, + "description": "Ingrese la clave API obtenida de ecobee.com.", + "title": "Clave API ecobee" } } } diff --git a/homeassistant/components/ecobee/translations/no.json b/homeassistant/components/ecobee/translations/no.json index 560fe4cf4e1..048659ac83d 100644 --- a/homeassistant/components/ecobee/translations/no.json +++ b/homeassistant/components/ecobee/translations/no.json @@ -9,14 +9,14 @@ }, "step": { "authorize": { - "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 Send.", - "title": "Autoriser app p\u00e5 ecobee.com" + "description": "Vennligst godkjenn denne appen p\u00e5 [https://www.ecobee.com/consumerportal](https://www.ecobee.com/consumerportal) med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 send.", + "title": "Godkjenn app p\u00e5 ecobee.com" }, "user": { "data": { "api_key": "API-n\u00f8kkel" }, - "description": "Vennligst skriv inn API-n\u00f8kkel som er innhentet fra ecobee.com.", + "description": "Vennligst fyll inn API-n\u00f8kkel som er innhentet fra ecobee.com.", "title": "ecobee API-n\u00f8kkel" } } diff --git a/homeassistant/components/ecobee/translations/pl.json b/homeassistant/components/ecobee/translations/pl.json index c0fed1d075c..240acabd093 100644 --- a/homeassistant/components/ecobee/translations/pl.json +++ b/homeassistant/components/ecobee/translations/pl.json @@ -14,10 +14,10 @@ }, "user": { "data": { - "api_key": "Klucz API" + "api_key": "[%key_id:common::config_flow::data::api_key%]" }, "description": "Prosz\u0119 wprowadzi\u0107 klucz API uzyskany na ecobee.com.", - "title": "Klucz API" + "title": "[%key_id:common::config_flow::data::api_key%]" } } } diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 59afe1351f5..0c31e3e50e0 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -16,7 +16,7 @@ from homeassistant.components.water_heater import ( STATE_PERFORMANCE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -120,7 +120,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class EcoNetWaterHeater(WaterHeaterDevice): +class EcoNetWaterHeater(WaterHeaterEntity): """Representation of an EcoNet water heater.""" def __init__(self, water_heater): diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 8b6115970bb..6ad51e6c474 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -14,7 +14,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice, + VacuumEntity, ) from homeassistant.helpers.icon import icon_for_battery_level @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(vacuums, True) -class EcovacsVacuum(VacuumDevice): +class EcovacsVacuum(VacuumEntity): """Ecovacs Vacuums such as Deebot.""" def __init__(self, device): diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index e44ec23bca7..17a43f36235 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -4,7 +4,7 @@ import logging from pyedimax.smartplug import SmartPlug 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_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)], True) -class SmartPlugSwitch(SwitchDevice): +class SmartPlugSwitch(SwitchEntity): """Representation an Edimax Smart Plug switch.""" def __init__(self, smartplug, name): diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index a4e0ca734fa..c33c9765730 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -8,6 +8,7 @@ from sml.asyncio import SmlProtocol import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -15,6 +16,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import Optional from homeassistant.util.dt import utcnow @@ -26,7 +28,12 @@ ICON_POWER = "mdi:flash" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SIGNAL_EDL21_TELEGRAM = "edl21_telegram" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_SERIAL_PORT): cv.string}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SERIAL_PORT): cv.string, + vol.Optional(CONF_NAME, default=""): cv.string, + }, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -54,6 +61,14 @@ class EDL21: # D=17: Time integral 7 # E=0: Total "1-0:1.17.0*255": "Last signed positive active energy total", + # C=2: Active power - + # D=8: Time integral 1 + # E=0: Total + "1-0:2.8.0*255": "Negative active energy total", + # E=1: Rate 1 + "1-0:2.8.1*255": "Negative active energy in tariff T1", + # E=2: Rate 2 + "1-0:2.8.2*255": "Negative active energy in tariff T2", # C=15: Active power absolute # D=7: Instantaneous value # E=0: Total @@ -74,6 +89,7 @@ class EDL21: self._registered_obis = set() self._hass = hass self._async_add_entities = async_add_entities + self._name = config[CONF_NAME] self._proto = SmlProtocol(config[CONF_SERIAL_PORT]) self._proto.add_listener(self.event, ["SmlGetListResponse"]) @@ -85,19 +101,35 @@ class EDL21: """Handle events from pysml.""" assert isinstance(message_body, SmlGetListResponse) + electricity_id = None + for telegram in message_body.get("valList", []): + if telegram.get("objName") == "1-0:0.0.9*255": + electricity_id = telegram.get("value") + break + + if electricity_id is None: + return + electricity_id = electricity_id.replace(" ", "") + new_entities = [] for telegram in message_body.get("valList", []): obis = telegram.get("objName") if not obis: continue - if obis in self._registered_obis: - async_dispatcher_send(self._hass, SIGNAL_EDL21_TELEGRAM, telegram) + if (electricity_id, obis) in self._registered_obis: + async_dispatcher_send( + self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram + ) else: name = self._OBIS_NAMES.get(obis) if name: - new_entities.append(EDL21Entity(obis, name, telegram)) - self._registered_obis.add(obis) + if self._name: + name = f"{self._name}: {name}" + new_entities.append( + EDL21Entity(electricity_id, obis, name, telegram) + ) + self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: _LOGGER.warning( "Unhandled sensor %s detected. Please report at " @@ -107,16 +139,41 @@ class EDL21: self._OBIS_BLACKLIST.add(obis) if new_entities: - self._async_add_entities(new_entities, update_before_add=True) + self._hass.loop.create_task(self.add_entities(new_entities)) + + async def add_entities(self, new_entities) -> None: + """Migrate old unique IDs, then add entities to hass.""" + registry = await async_get_registry(self._hass) + + for entity in new_entities: + old_entity_id = registry.async_get_entity_id( + "sensor", DOMAIN, entity.old_unique_id + ) + if old_entity_id is not None: + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + entity.old_unique_id, + entity.unique_id, + ) + if registry.async_get_entity_id("sensor", DOMAIN, entity.unique_id): + registry.async_remove(old_entity_id) + else: + registry.async_update_entity( + old_entity_id, new_unique_id=entity.unique_id + ) + + self._async_add_entities(new_entities, update_before_add=True) class EDL21Entity(Entity): """Entity reading values from EDL21 telegram.""" - def __init__(self, obis, name, telegram): + def __init__(self, electricity_id, obis, name, telegram): """Initialize an EDL21Entity.""" + self._electricity_id = electricity_id self._obis = obis self._name = name + self._unique_id = f"{electricity_id}_{obis}" self._telegram = telegram self._min_time = MIN_TIME_BETWEEN_UPDATES self._last_update = utcnow() @@ -132,8 +189,10 @@ class EDL21Entity(Entity): """Run when entity about to be added to hass.""" @callback - def handle_telegram(telegram): + def handle_telegram(electricity_id, telegram): """Update attributes from last received telegram for this object.""" + if self._electricity_id != electricity_id: + return if self._obis != telegram.get("objName"): return if self._telegram == telegram: @@ -164,6 +223,11 @@ class EDL21Entity(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" + return self._unique_id + + @property + def old_unique_id(self) -> str: + """Return a less unique ID as used in the first version of edl21.""" return self._obis @property diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 7e5f88cff3e..b133a96b820 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([device], True) -class EgardiaAlarm(alarm.AlarmControlPanel): +class EgardiaAlarm(alarm.AlarmControlPanelEntity): """Representation of a Egardia alarm.""" def __init__( diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 4f02d6fdde0..4be443a36f4 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -1,7 +1,7 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import STATE_OFF, STATE_ON from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class EgardiaBinarySensor(BinarySensorDevice): +class EgardiaBinarySensor(BinarySensorEntity): """Represents a sensor based on an Egardia sensor (IR, Door Contact).""" def __init__(self, sensor_id, name, egardia_system, device_class): diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 7b801578ccd..803b20383b6 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Eight Sleep binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity @@ -25,7 +25,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(all_sensors, True) -class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice): +class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): """Representation of a Eight Sleep heat-based sensor.""" def __init__(self, name, eight, sensor): diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 99bca1ba20e..9dae0cd1f40 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME @@ -45,7 +45,7 @@ async def async_setup_entry( async_add_entities([ElgatoLight(entry.entry_id, elgato, info)], True) -class ElgatoLight(Light): +class ElgatoLight(LightEntity): """Defines a Elgato Key Light.""" def __init__( diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json index f2123731412..a00bf027451 100644 --- a/homeassistant/components/elgato/strings.json +++ b/homeassistant/components/elgato/strings.json @@ -4,7 +4,10 @@ "step": { "user": { "description": "Set up your Elgato Key Light to integrate with Home Assistant.", - "data": { "host": "Host or IP address", "port": "Port number" } + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } }, "zeroconf_confirm": { "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", @@ -19,4 +22,4 @@ "connection_error": "Failed to connect to Elgato Key Light device." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/en.json b/homeassistant/components/elgato/translations/en.json index 1a2ff6d97f0..d96b67c3210 100644 --- a/homeassistant/components/elgato/translations/en.json +++ b/homeassistant/components/elgato/translations/en.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "host": "Host or IP address", - "port": "Port number" + "host": "Host", + "port": "Port" }, "description": "Set up your Elgato Key Light to integrate with Home Assistant.", "title": "Link your Elgato Key Light" diff --git a/homeassistant/components/elgato/translations/es-419.json b/homeassistant/components/elgato/translations/es-419.json index 46a008009b9..9d12537851d 100644 --- a/homeassistant/components/elgato/translations/es-419.json +++ b/homeassistant/components/elgato/translations/es-419.json @@ -1,14 +1,24 @@ { "config": { + "abort": { + "already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.", + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", "step": { "user": { "data": { "host": "Host o direcci\u00f3n IP", "port": "N\u00famero de puerto" }, - "description": "Configure su Elgato Key Light para integrarse con Home Assistant." + "description": "Configure su Elgato Key Light para integrarse con Home Assistant.", + "title": "Vincule su Elgato Key Light" }, "zeroconf_confirm": { + "description": "\u00bfDesea agregar el disposiivo Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Dispositivo Elgato Key Light descubierto" } } diff --git a/homeassistant/components/elgato/translations/ko.json b/homeassistant/components/elgato/translations/ko.json index 50b8536ea05..fcb8922aaf1 100644 --- a/homeassistant/components/elgato/translations/ko.json +++ b/homeassistant/components/elgato/translations/ko.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", - "port": "\ud3ec\ud2b8 \ubc88\ud638" + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" }, "description": "Home Assistant \uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", - "title": "Elgato Key Light \uc5f0\uacb0" + "title": "Elgato Key Light \uc5f0\uacb0\ud558\uae30" }, "zeroconf_confirm": { "description": "Elgato Key Light \uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json index 344c2c086ea..769495ee949 100644 --- a/homeassistant/components/elgato/translations/pl.json +++ b/homeassistant/components/elgato/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "To urz\u0105dzenie Elgato Key Light jest ju\u017c skonfigurowane.", + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." }, "error": { @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port" + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]" }, "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistant'em.", "title": "Po\u0142\u0105cz swoje Elgato Key Light" diff --git a/homeassistant/components/elgato/translations/ru.json b/homeassistant/components/elgato/translations/ru.json index a09a00b840c..d8bcb43520a 100644 --- a/homeassistant/components/elgato/translations/ru.json +++ b/homeassistant/components/elgato/translations/ru.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", - "port": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430" + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Elgato Key Light \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", "title": "Elgato Key Light" diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 87fe1df0633..03b71b70d77 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index b217988f8d8..3e9ab114837 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( ATTR_CHANGED_BY, FORMAT_NUMBER, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -109,7 +109,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class ElkArea(ElkAttachedEntity, AlarmControlPanel, RestoreEntity): +class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): """Representation of an Area / Partition within the ElkM1 alarm panel.""" def __init__(self, element, elk, elk_data): diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index baaf3d44eb2..6d10df45adf 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,7 +1,7 @@ """Support for control of Elk-M1 connected thermostats.""" from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class ElkThermostat(ElkEntity, ClimateDevice): +class ElkThermostat(ElkEntity, ClimateEntity): """Representation of an Elk-M1 Thermostat.""" def __init__(self, element, elk, elk_data): diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index b7cfe20dfd8..19a09d13975 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -1,6 +1,10 @@ """Support for control of ElkM1 lighting (X10, UPB, etc).""" -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from . import ElkEntity, create_elk_entities from .const import DOMAIN @@ -15,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class ElkLight(ElkEntity, Light): +class ElkLight(ElkEntity, LightEntity): """Representation of an Elk lighting device.""" def __init__(self, element, elk, elk_data): diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index be7d0aa1d74..223e2ca3fff 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -7,8 +7,8 @@ "data": { "protocol": "Protocol", "address": "The IP address or domain or serial port if connecting via serial.", - "username": "Username (secure only).", - "password": "Password (secure only).", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", "prefix": "A unique prefix (leave blank if you only have one ElkM1).", "temperature_unit": "The temperature unit ElkM1 uses." } @@ -24,4 +24,4 @@ "address_already_configured": "An ElkM1 with this address is already configured" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index af32e81bc4c..d9eb59737b6 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -1,5 +1,5 @@ """Support for control of ElkM1 outputs (relays).""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import ElkAttachedEntity, create_elk_entities from .const import DOMAIN @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class ElkOutput(ElkAttachedEntity, SwitchDevice): +class ElkOutput(ElkAttachedEntity, SwitchEntity): """Elk output as switch.""" @property diff --git a/homeassistant/components/elkm1/translations/en.json b/homeassistant/components/elkm1/translations/en.json index 784a9feb642..7fef25d79a6 100644 --- a/homeassistant/components/elkm1/translations/en.json +++ b/homeassistant/components/elkm1/translations/en.json @@ -13,11 +13,11 @@ "user": { "data": { "address": "The IP address or domain or serial port if connecting via serial.", - "password": "Password (secure only).", + "password": "Password", "prefix": "A unique prefix (leave blank if you only have one ElkM1).", "protocol": "Protocol", "temperature_unit": "The temperature unit ElkM1 uses.", - "username": "Username (secure only)." + "username": "Username" }, "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", "title": "Connect to Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/es-419.json b/homeassistant/components/elkm1/translations/es-419.json new file mode 100644 index 00000000000..02271c4ea6c --- /dev/null +++ b/homeassistant/components/elkm1/translations/es-419.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un ElkM1 con esta direcci\u00f3n ya est\u00e1 configurado", + "already_configured": "Un ElkM1 con este prefijo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "address": "La direcci\u00f3n IP, dominio o puerto serie si se conecta via serial.", + "password": "Contrase\u00f1a (solo segura).", + "prefix": "Un prefijo \u00fanico (d\u00e9jelo en blanco si solo tiene un ElkM1).", + "protocol": "Protocolo", + "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", + "username": "Nombre de usuario (solo seguro)." + }, + "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. La velocidad en baudios es opcional y su valor predeterminado es 115200.", + "title": "Con\u00e9ctese al control Elk-M1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 27eac521430..2dfa3ba8347 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -14,6 +14,7 @@ "data": { "address": "L'adresse IP ou le domaine ou le port s\u00e9rie si vous vous connectez via s\u00e9rie.", "password": "Mot de passe (s\u00e9curis\u00e9 uniquement).", + "prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).", "protocol": "Protocole", "username": "Nom d'utilisateur (s\u00e9curis\u00e9 uniquement)." }, diff --git a/homeassistant/components/elkm1/translations/ko.json b/homeassistant/components/elkm1/translations/ko.json index 96837972007..0b88308547a 100644 --- a/homeassistant/components/elkm1/translations/ko.json +++ b/homeassistant/components/elkm1/translations/ko.json @@ -13,11 +13,11 @@ "user": { "data": { "address": "\uc2dc\ub9ac\uc5bc\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub294 \uacbd\uc6b0\uc758 IP \uc8fc\uc18c \ub098 \ub3c4\uba54\uc778 \ub610\ub294 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8", - "password": "\ube44\ubc00\ubc88\ud638 (\ubcf4\uc548 \uc804\uc6a9).", + "password": "\ube44\ubc00\ubc88\ud638", "prefix": "\uace0\uc720\ud55c \uc811\ub450\uc0ac (ElkM1 \uc774 \ud558\ub098\ub9cc \uc788\uc73c\uba74 \ube44\uc6cc\ub450\uc138\uc694).", "protocol": "\ud504\ub85c\ud1a0\ucf5c", "temperature_unit": "ElkM1 \uc774 \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 (\ubcf4\uc548 \uc804\uc6a9)." + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \ud1b5\uc2e0\uc18d\ub3c4 \ubc14\uc6b0\ub4dc\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.", "title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/elkm1/translations/lb.json b/homeassistant/components/elkm1/translations/lb.json index 6e1af6353f8..0579e1474f6 100644 --- a/homeassistant/components/elkm1/translations/lb.json +++ b/homeassistant/components/elkm1/translations/lb.json @@ -19,7 +19,7 @@ "temperature_unit": "Temperatur Eenheet d\u00e9i den ElkM1 benotzt.", "username": "Benotzernumm (n\u00ebmmen ges\u00e9chert)" }, - "description": "D'Adress muss an der Form 'adress[:port]' fir 'ges\u00e9chert' an 'onges\u00e9chert' sinn. Beispill: '192.168.1.1'. De Port os optionell an ass standardm\u00e9isseg op 2101 fir 'onges\u00e9chert' an op 2601 fir 'ges\u00e9chert' d\u00e9fin\u00e9iert. Fir de serielle Protokoll, muss d'Adress an der Form 'tty[:baud]' sinn. Beispill: '/dev/ttyS1'. Baud Rate ass optionell an ass standardmlisseg op 115200 d\u00e9fin\u00e9iert.", + "description": "D'Adress muss an der Form 'adress[:port]' fir 'ges\u00e9chert' an 'onges\u00e9chert' sinn. Beispill: '192.168.1.1'. De Port os optionell an ass standardm\u00e9isseg op 2101 fir 'onges\u00e9chert' an op 2601 fir 'ges\u00e9chert' d\u00e9fin\u00e9iert. Fir de serielle Protokoll, muss d'Adress an der Form 'tty[:baud]' sinn. Beispill: '/dev/ttyS1'. Baud Rate ass optionell an ass standardm\u00e9isseg op 115200 d\u00e9fin\u00e9iert.", "title": "Mat Elk-M1 Control verbannen" } } diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json new file mode 100644 index 00000000000..9e7adf71c4b --- /dev/null +++ b/homeassistant/components/elkm1/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "address_already_configured": "Een ElkM1 met dit adres is al geconfigureerd", + "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.", + "password": "Wachtwoord (alleen beveiligd).", + "prefix": "Een uniek voorvoegsel (laat dit leeg als u maar \u00e9\u00e9n ElkM1 heeft).", + "protocol": "Protocol", + "temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.", + "username": "Gebruikersnaam (alleen beveiligd)." + }, + "description": "De adresreeks moet de vorm 'adres [: poort]' hebben voor 'veilig' en 'niet-beveiligd'. Voorbeeld: '192.168.1.1'. De poort is optioneel en is standaard 2101 voor 'niet beveiligd' en 2601 voor 'beveiligd'. Voor het seri\u00eble protocol moet het adres de vorm 'tty [: baud]' hebben. Voorbeeld: '/ dev / ttyS1'. De baud is optioneel en is standaard ingesteld op 115200.", + "title": "Maak verbinding met Elk-M1 Control" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/pl.json b/homeassistant/components/elkm1/translations/pl.json index 3a445b1008c..f630376a374 100644 --- a/homeassistant/components/elkm1/translations/pl.json +++ b/homeassistant/components/elkm1/translations/pl.json @@ -6,18 +6,18 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { "address": "Adres IP, domena lub port szeregowy w przypadku po\u0142\u0105czenia szeregowego.", - "password": "Has\u0142o (tylko bezpieczne).", + "password": "[%key_id:common::config_flow::data::password%] (tylko bezpieczne)", "prefix": "Unikatowy prefiks (pozostaw pusty, je\u015bli masz tylko jeden ElkM1).", "protocol": "Protok\u00f3\u0142", "temperature_unit": "Jednostka temperatury u\u017cywanej przez ElkM1.", - "username": "Nazwa u\u017cytkownika (tylko bezpieczne)" + "username": "[%key_id:common::config_flow::data::username%] (tylko bezpieczne)" }, "description": "Adres musi by\u0107 w postaci 'adres[:port]' dla tryb\u00f3w 'zabezpieczony' i 'niezabezpieczony'. Przyk\u0142ad: '192.168.1.1'. Port jest opcjonalny i domy\u015blnie ustawiony na 2101 dla po\u0142\u0105cze\u0144 'niezabezpieczonych' i 2601 dla 'zabezpieczonych'. W przypadku protoko\u0142u szeregowego adres musi by\u0107 w formie 'tty[:baudrate]'. Przyk\u0142ad: '/dev/ttyS1'. Warto\u015b\u0107 transmisji jest opcjonalna i domy\u015blnie wynosi 115200.", "title": "Pod\u0142\u0105czenie do sterownika Elk-M1" diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json index 45877e16992..94d3a47a8e1 100644 --- a/homeassistant/components/elkm1/translations/ru.json +++ b/homeassistant/components/elkm1/translations/ru.json @@ -12,14 +12,14 @@ "step": { "user": { "data": { - "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure')", - "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1).", + "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1)", "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", - "username": "\u041b\u043e\u0433\u0438\u043d (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure')" + "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0421\u0442\u0440\u043e\u043a\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'addres[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'non-secure' \u0438 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'serial' \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0440\u0430\u0432\u0435\u043d 115200.", + "description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432 'secure' \u0438 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'non-secure' \u0438 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'serial' \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 115200.", "title": "Elk-M1 Control" } } diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json new file mode 100644 index 00000000000..23a7d475a6f --- /dev/null +++ b/homeassistant/components/elkm1/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/zh-Hant.json b/homeassistant/components/elkm1/translations/zh-Hant.json index 7362ec42e93..01c1197f66a 100644 --- a/homeassistant/components/elkm1/translations/zh-Hant.json +++ b/homeassistant/components/elkm1/translations/zh-Hant.json @@ -13,11 +13,11 @@ "user": { "data": { "address": "IP \u6216\u7db2\u57df\u540d\u7a31\u3001\u5e8f\u5217\u57e0\uff08\u5047\u5982\u900f\u904e\u5e8f\u5217\u9023\u7dda\uff09\u3002", - "password": "\u5bc6\u78bc\uff08\u50c5\u52a0\u5bc6\uff09\u3002", + "password": "\u5bc6\u78bc", "prefix": "\u7368\u4e00\u7684 Prefix\uff08\u5047\u5982\u50c5\u6709\u4e00\u7d44 ElkM1 \u5247\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "protocol": "\u901a\u8a0a\u5354\u5b9a", "temperature_unit": "ElkM1 \u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", - "username": "\u4f7f\u7528\u8005\u540d\u7a31\uff08\u50c5\u52a0\u5bc6\uff09\u3002" + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u52a0\u5bc6\u8207\u975e\u52a0\u5bc6\u4e4b\u4f4d\u5740\u5b57\u4e32\u683c\u5f0f\u5fc5\u9808\u70ba 'address[:port]'\u3002\u4f8b\u5982\uff1a'192.168.1.1'\u3002\u901a\u8a0a\u57e0\u70ba\u9078\u9805\u8f38\u5165\uff0c\u975e\u52a0\u5bc6\u9810\u8a2d\u503c\u70ba 2101\u3001\u52a0\u5bc6\u5247\u70ba 2601\u3002\u5e8f\u5217\u901a\u8a0a\u5354\u5b9a\u3001\u4f4d\u5740\u683c\u5f0f\u5fc5\u9808\u70ba 'tty[:baud]'\u3002\u4f8b\u5982\uff1a'/dev/ttyS1'\u3002\u50b3\u8f38\u7387\u70ba\u9078\u9805\u8f38\u5165\uff0c\u9810\u8a2d\u503c\u70ba 115200\u3002", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index d867d286f50..12b21c23d1a 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -4,7 +4,7 @@ import logging import pypca from serial import SerialException -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): pca.start_scan() -class SmartPlugSwitch(SwitchDevice): +class SmartPlugSwitch(SwitchEntity): """Representation of a PCA Smart Plug switch.""" def __init__(self, pca, device_id): diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 0a14799ce24..7872b8215a6 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -4,7 +4,7 @@ import logging from pyemby import EmbyServer import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -134,7 +134,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby) -class EmbyDevice(MediaPlayerDevice): +class EmbyDevice(MediaPlayerEntity): """Representation of an Emby device.""" def __init__(self, emby, device_id): diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index da6e7acab40..5dbd52d09b1 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -14,6 +14,7 @@ from homeassistant.util.json import load_json, save_json from .hue_api import ( HueAllGroupsStateView, HueAllLightsStateView, + HueConfigView, HueFullStateView, HueGroupView, HueOneLightChangeView, @@ -119,6 +120,7 @@ async def async_setup(hass, yaml_config): HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) HueFullStateView(config).register(app, app.router) + HueConfigView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 9637b0fb371..069a4b60d0c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -89,7 +89,7 @@ HUE_API_STATE_SAT_MAX = 254 HUE_API_STATE_CT_MIN = 153 # Color temp HUE_API_STATE_CT_MAX = 500 -HUE_API_USERNAME = "12345678901234567890" +HUE_API_USERNAME = "nouser" UNAUTHORIZED_USER = [ {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} ] @@ -226,14 +226,47 @@ class HueFullStateView(HomeAssistantView): "config": { "mac": "00:00:00:00:00:00", "swversion": "01003542", + "apiversion": "1.17.0", "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + "linkbutton": True, }, } return self.json(json_response) +class HueConfigView(HomeAssistantView): + """Return config view of emulated hue.""" + + url = "/api/{username}/config" + name = "emulated_hue:username:config" + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to get the configuration.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "apiversion": "1.17.0", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + "linkbutton": True, + } + + return self.json(json_response) + + class HueOneLightStateView(HomeAssistantView): """Handle requests for getting info about a single entity.""" @@ -506,7 +539,9 @@ class HueOneLightChangeView(HomeAssistantView): # Create success responses for all received keys json_response = [ - create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) + create_hue_success_response( + entity_number, HUE_API_STATE_ON, parsed[STATE_ON] + ) ] for (key, val) in ( @@ -517,7 +552,7 @@ class HueOneLightChangeView(HomeAssistantView): ): if parsed[key] is not None: json_response.append( - create_hue_success_response(entity_id, val, parsed[key]) + create_hue_success_response(entity_number, val, parsed[key]) ) return self.json(json_response) @@ -710,9 +745,9 @@ def entity_to_json(config, entity): return retval -def create_hue_success_response(entity_id, attr, value): +def create_hue_success_response(entity_number, attr, value): """Create a success response for an attribute set on a light.""" - success_key = f"/lights/{entity_id}/state/{attr}" + success_key = f"/lights/{entity_number}/state/{attr}" return {"success": {success_key: value}} diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index c10fb3b826b..f0fe392f865 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -42,7 +42,7 @@ class DescriptionXmlView(HomeAssistantView): Philips hue bridge 2015 BSB002 http://www.meethue.com -1234 +001788FFFE23BFC2 uuid:2f402f80-da50-11e1-9b23-001788255acc @@ -77,10 +77,10 @@ class UPNPResponderThread(threading.Thread): CACHE-CONTROL: max-age=60 EXT: LOCATION: http://{advertise_ip}:{advertise_port}/description.xml -SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1 -hue-bridgeid: 1234 -ST: urn:schemas-upnp-org:device:basic:1 -USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: 001788FFFE23BFC2 +ST: upnp:rootdevice +USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice """ diff --git a/homeassistant/components/emulated_roku/translations/es-419.json b/homeassistant/components/emulated_roku/translations/es-419.json index 85d75c81ff3..9aa7ff0bc92 100644 --- a/homeassistant/components/emulated_roku/translations/es-419.json +++ b/homeassistant/components/emulated_roku/translations/es-419.json @@ -7,7 +7,9 @@ "user": { "data": { "host_ip": "IP del host", - "name": "Nombre" + "listen_port": "Puerto de escucha", + "name": "Nombre", + "upnp_bind_multicast": "Enlazar multidifusi\u00f3n (verdadero/falso)" }, "title": "Definir la configuraci\u00f3n del servidor." } diff --git a/homeassistant/components/emulated_roku/translations/fi.json b/homeassistant/components/emulated_roku/translations/fi.json new file mode 100644 index 00000000000..bad7fe71985 --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/fi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "name_exists": "Nimi on jo olemassa" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Mainosta IP-osoitetta" + }, + "title": "M\u00e4\u00e4rit\u00e4 palvelimen asetukset" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/ko.json b/homeassistant/components/emulated_roku/translations/ko.json index 3e1062e4201..d2f8d425a39 100644 --- a/homeassistant/components/emulated_roku/translations/ko.json +++ b/homeassistant/components/emulated_roku/translations/ko.json @@ -13,9 +13,9 @@ "name": "\uc774\ub984", "upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ud560\ub2f9 (\ucc38/\uac70\uc9d3)" }, - "title": "\uc11c\ubc84 \uad6c\uc131 \uc815\uc758" + "title": "\uc11c\ubc84 \uad6c\uc131 \uc815\uc758\ud558\uae30" } } }, - "title": "EmulatedRoku" + "title": "\uc5d0\ubbac\ub808\uc774\ud2b8 \ub41c Roku" } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/no.json b/homeassistant/components/emulated_roku/translations/no.json index 2d4f72c50fb..8bd1d4793a5 100644 --- a/homeassistant/components/emulated_roku/translations/no.json +++ b/homeassistant/components/emulated_roku/translations/no.json @@ -17,5 +17,5 @@ } } }, - "title": "Emulerte Roku" + "title": "Emulert Roku" } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/pl.json b/homeassistant/components/emulated_roku/translations/pl.json index 6ce1c17e5d8..00625d2097c 100644 --- a/homeassistant/components/emulated_roku/translations/pl.json +++ b/homeassistant/components/emulated_roku/translations/pl.json @@ -7,9 +7,9 @@ "user": { "data": { "advertise_ip": "IP rozg\u0142aszania", - "advertise_port": "Port rozg\u0142aszania", + "advertise_port": "[%key_id:common::config_flow::data::port%] rozg\u0142aszania", "host_ip": "Adres IP", - "listen_port": "Port nas\u0142uchu", + "listen_port": "[%key_id:common::config_flow::data::port%] nas\u0142uchu", "name": "Nazwa", "upnp_bind_multicast": "Powi\u0105\u017c multicast (prawda/fa\u0142sz)" }, diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index f8a341481a8..563b2c5195d 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -4,7 +4,7 @@ import logging from openwebif.api import CreateDevice import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, @@ -117,7 +117,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([Enigma2Device(config[CONF_NAME], device)], True) -class Enigma2Device(MediaPlayerDevice): +class Enigma2Device(MediaPlayerEntity): """Representation of an Enigma2 box.""" def __init__(self, name, device): diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 4ff1b461129..7fb8ea5e3f2 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components import enocean from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)]) -class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): +class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorEntity): """Representation of EnOcean binary sensors such as wall switches. Supported EEPs (EnOcean Equipment Profiles): diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index a1d2b22cdb4..0df0c94775a 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanLight(sender_id, dev_id, dev_name)]) -class EnOceanLight(enocean.EnOceanDevice, Light): +class EnOceanLight(enocean.EnOceanDevice, LightEntity): """Representation of an EnOcean light source.""" def __init__(self, sender_id, dev_id, dev_name): diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index bde6c16bdfe..0b4fbaf88ee 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,6 +2,6 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.11.0"], + "requirements": ["envoy_reader==0.16.1"], "codeowners": [] } diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 62c57daf19d..670dc78392f 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return True -class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanel): +class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): """Representation of an Envisalink-based alarm panel.""" def __init__( diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index f698a9d27d9..54445660484 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -2,7 +2,7 @@ import datetime import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_LAST_TRIP_TIME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): +class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): """Representation of an Envisalink binary sensor.""" def __init__(self, hass, zone_number, zone_name, zone_type, info, controller): diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index d743f3e82ba..787677a6605 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -15,7 +15,7 @@ from pyephember.pyephember import ( ) import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return -class EphEmberThermostat(ClimateDevice): +class EphEmberThermostat(ClimateEntity): """Representation of a EphEmber thermostat.""" def __init__(self, ember, zone): diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 6a04988bebb..df0dcc536b5 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -26,7 +26,7 @@ from epson_projector.const import ( ) import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -124,7 +124,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class EpsonProjector(MediaPlayerDevice): +class EpsonProjector(MediaPlayerEntity): """Representation of Epson Projector Device.""" def __init__(self, websession, name, host, port, encryption): diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index d0b60c74443..402dfc684b3 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -6,7 +6,7 @@ from bluepy.btle import BTLEException import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, @@ -76,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class EQ3BTSmartThermostat(ClimateDevice): +class EQ3BTSmartThermostat(ClimateEntity): """Representation of an eQ-3 Bluetooth Smart thermostat.""" def __init__(self, _mac, _name): diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index fe41bb2f7bb..d605a48410b 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -3,7 +3,7 @@ from typing import Optional from aioesphomeapi import BinarySensorInfo, BinarySensorState -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import EsphomeEntity, platform_async_setup_entry @@ -21,7 +21,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class EsphomeBinarySensor(EsphomeEntity, BinarySensorDevice): +class EsphomeBinarySensor(EsphomeEntity, BinarySensorEntity): """A binary sensor implementation for ESPHome.""" @property diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 960366a8332..46ed214afba 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -11,7 +11,7 @@ from aioesphomeapi import ( ClimateSwingMode, ) -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -75,7 +75,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities, component_key="climate", info_type=ClimateInfo, - entity_type=EsphomeClimateDevice, + entity_type=EsphomeClimateEntity, state_type=ClimateState, ) @@ -129,7 +129,7 @@ def _swing_modes(): } -class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): +class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): """A climate implementation for ESPHome.""" @property diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 17d3ed5f659..cb9b7958efa 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -5,18 +5,21 @@ from typing import Optional from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol -from homeassistant import config_entries, core -from homeassistant.helpers.typing import ConfigType +from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .entry_data import DATA_KEY, RuntimeEntryData +DOMAIN = "esphome" -@config_entries.HANDLERS.register("esphome") -class EsphomeFlowHandler(config_entries.ConfigFlow): + +class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a esphome config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize flow.""" @@ -32,8 +35,8 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): return await self._async_authenticate_or_add(user_input) fields = OrderedDict() - fields[vol.Required("host", default=self._host or vol.UNDEFINED)] = str - fields[vol.Optional("port", default=self._port or 6053)] = int + fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str + fields[vol.Optional(CONF_PORT, default=self._port or 6053)] = int errors = {} if error is not None: @@ -46,19 +49,19 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): @property def _name(self): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - return self.context.get("name") + return self.context.get(CONF_NAME) @_name.setter def _name(self, value): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["name"] = value + self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} def _set_user_input(self, user_input): if user_input is None: return - self._host = user_input["host"] - self._port = user_input["port"] + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] async def _async_authenticate_or_add(self, user_input): self._set_user_input(user_input) @@ -81,56 +84,69 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): step_id="discovery_confirm", description_placeholders={"name": self._name} ) - async def async_step_zeroconf(self, user_input: ConfigType): + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. - local_name = user_input["hostname"][:-1] + local_name = discovery_info["hostname"][:-1] node_name = local_name[: -len(".local")] - address = user_input["properties"].get("address", local_name) + address = discovery_info["properties"].get("address", local_name) # Check if already configured + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured( + updates={CONF_HOST: discovery_info[CONF_HOST]} + ) + for entry in self._async_current_entries(): already_configured = False - if entry.data["host"] == address: - # Is this address already configured? + + if ( + entry.data[CONF_HOST] == address + or entry.data[CONF_HOST] == discovery_info[CONF_HOST] + ): + # Is this address or IP address already configured? already_configured = True elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): # Does a config entry with this name already exist? data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id] + # Node names are unique in the network if data.device_info is not None: already_configured = data.device_info.name == node_name if already_configured: + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_HOST: discovery_info[CONF_HOST]}, + unique_id=node_name, + ) + return self.async_abort(reason="already_configured") - self._host = address - self._port = user_input["port"] + self._host = discovery_info[CONF_HOST] + self._port = discovery_info[CONF_PORT] self._name = node_name - # Check if flow for this device already in progress - for flow in self._async_in_progress(): - if flow["context"].get("name") == node_name: - return self.async_abort(reason="already_configured") - return await self.async_step_discovery_confirm() - @core.callback + @callback def _async_get_entry(self): return self.async_create_entry( title=self._name, data={ - "host": self._host, - "port": self._port, + CONF_HOST: self._host, + CONF_PORT: self._port, # The API uses protobuf, so empty string denotes absence - "password": self._password or "", + CONF_PASSWORD: self._password or "", }, ) async def async_step_authenticate(self, user_input=None, error=None): """Handle getting password for authentication.""" if user_input is not None: - self._password = user_input["password"] + self._password = user_input[CONF_PASSWORD] error = await self.try_login() if error: return await self.async_step_authenticate(error=error) diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 53014991de8..fcf7c22a2a2 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -14,7 +14,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -39,7 +39,7 @@ async def async_setup_entry( ) -class EsphomeCover(EsphomeEntity, CoverDevice): +class EsphomeCover(EsphomeEntity, CoverEntity): """A cover implementation for ESPHome.""" @property diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 9a2a0ccd0bc..36c22f28016 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -50,7 +50,7 @@ async def async_setup_entry( ) -class EsphomeLight(EsphomeEntity, Light): +class EsphomeLight(EsphomeEntity, LightEntity): """A switch implementation for ESPHome.""" @property diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 0c2f0fee8d9..af7515245b5 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -1,6 +1,9 @@ { "config": { - "abort": { "already_configured": "ESP is already configured" }, + "abort": { + "already_configured": "ESP is already configured", + "already_in_progress": "ESP configuration is already in progress" + }, "error": { "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips", "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", @@ -8,11 +11,16 @@ }, "step": { "user": { - "data": { "host": "Host", "port": "Port" }, + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." }, "authenticate": { - "data": { "password": "Password" }, + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, "description": "Please enter the password you set in your configuration for {name}." }, "discovery_confirm": { @@ -22,4 +30,4 @@ }, "flow_title": "ESPHome: {name}" } -} +} \ No newline at end of file diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index b52d630e1b4..a3c7eeab946 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -4,7 +4,7 @@ from typing import Optional from aioesphomeapi import SwitchInfo, SwitchState -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -28,7 +28,7 @@ async def async_setup_entry( ) -class EsphomeSwitch(EsphomeEntity, SwitchDevice): +class EsphomeSwitch(EsphomeEntity, SwitchEntity): """A switch implementation for ESPHome.""" @property diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 6cd1405b824..9f9378081dc 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ja est\u00e0 configurat" + "already_configured": "ESP ja est\u00e0 configurat", + "already_in_progress": "La configuraci\u00f3 de l'ESP ja est\u00e0 en curs" }, "error": { "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json index b8c245a66e4..36a600befaa 100644 --- a/homeassistant/components/esphome/translations/cs.json +++ b/homeassistant/components/esphome/translations/cs.json @@ -1,14 +1,33 @@ { "config": { + "abort": { + "already_configured": "Tento ESP uzel je ji\u017e nakonfigurov\u00e1n", + "already_in_progress": "Konfigurace uzlu ESP ji\u017e prob\u00edh\u00e1" + }, + "error": { + "connection_error": "Nelze se p\u0159ipojit k ESP. Zkontrolujte, zda va\u0161e YAML konfigurace obsahuje \u0159\u00e1dek 'api:'.", + "invalid_password": "Neplatn\u00e9 heslo", + "resolve_error": "Nelze naj\u00edt IP adresu uzlu ESP. Pokud tato chyba p\u0159etrv\u00e1v\u00e1, nastavte statickou adresu IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { - "description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} ." + "data": { + "password": "Heslo" + }, + "description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} .", + "title": "Zadejte heslo" }, "discovery_confirm": { "description": "Chcete do domovsk\u00e9ho asistenta p\u0159idat uzel ESPHome `{name}`?", "title": "Nalezen uzel ESPHome" }, "user": { + "data": { + "host": "Adresa uzlu", + "port": "Port" + }, + "description": "Zadejte pros\u00edm nastaven\u00ed p\u0159ipojen\u00ed va\u0161eho [ESPHome](https://esphomelib.com/) uzlu.", "title": "[%key:component::esphome::title%]" } } diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 64262aa654a..299660cb348 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ist bereits konfiguriert" + "already_configured": "ESP ist bereits konfiguriert", + "already_in_progress": "Die ESP-Konfiguration wird bereits ausgef\u00fchrt" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index b52dbc5a3f1..3cc24dea78e 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP is already configured" + "already_configured": "ESP is already configured", + "already_in_progress": "ESP configuration is already in progress" }, "error": { "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", diff --git a/homeassistant/components/esphome/translations/es-419.json b/homeassistant/components/esphome/translations/es-419.json index 7bbe61aceb2..2774ff7ea68 100644 --- a/homeassistant/components/esphome/translations/es-419.json +++ b/homeassistant/components/esphome/translations/es-419.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ya est\u00e1 configurado" + "already_configured": "ESP ya est\u00e1 configurado", + "already_in_progress": "La configuraci\u00f3n de ESP ya est\u00e1 en progreso" }, "error": { "connection_error": "No se puede conectar a ESP. Aseg\u00farese de que su archivo YAML contenga una l\u00ednea 'api:'.", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index ff2f1f093c9..042ac419503 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ya est\u00e1 configurado" + "already_configured": "ESP ya est\u00e1 configurado", + "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha" }, "error": { "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", diff --git a/homeassistant/components/esphome/translations/fi.json b/homeassistant/components/esphome/translations/fi.json index 9d6d417a053..3be6a5a5142 100644 --- a/homeassistant/components/esphome/translations/fi.json +++ b/homeassistant/components/esphome/translations/fi.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "ESP on jo m\u00e4\u00e4ritetty" + }, + "flow_title": "ESPHome: {name}", "step": { "user": { "title": "[%key:component::esphome::title%]" diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 8159b3cac2e..a6620218f0e 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "ESP est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration ESP est d\u00e9j\u00e0 en cours" }, "error": { "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 050c1222495..60ba2a31890 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u00e8 gi\u00e0 configurato" + "already_configured": "ESP \u00e8 gi\u00e0 configurato", + "already_in_progress": "La configurazione ESP \u00e8 gi\u00e0 in corso" }, "error": { "connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", diff --git a/homeassistant/components/esphome/translations/ko.json b/homeassistant/components/esphome/translations/ko.json index e89f3360b32..9ed518a9985 100644 --- a/homeassistant/components/esphome/translations/ko.json +++ b/homeassistant/components/esphome/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "ESP \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "ESP \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." }, "error": { "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", @@ -15,7 +16,7 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "description": "{name} \uc758 \uad6c\uc131\uc5d0 \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" + "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825\ud558\uae30" }, "discovery_confirm": { "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", @@ -27,7 +28,7 @@ "port": "\ud3ec\ud2b8" }, "description": "[ESPHome](https://esphomelib.com/) \ub178\ub4dc\uc758 \uc5f0\uacb0 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "[%key:component::esphome::title%]" + "title": "ESPHome" } } } diff --git a/homeassistant/components/esphome/translations/lb.json b/homeassistant/components/esphome/translations/lb.json index 9bfc78af28c..35d9c8472cf 100644 --- a/homeassistant/components/esphome/translations/lb.json +++ b/homeassistant/components/esphome/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ass scho konfigur\u00e9iert" + "already_configured": "ESP ass scho konfigur\u00e9iert", + "already_in_progress": "ESP Konfiguratioun ass schonn am gaang." }, "error": { "connection_error": "Keng Verbindung zum ESP. Iwwerpr\u00e9ift d'Pr\u00e4sens vun der Zeil api: am YAML Fichier.", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index c04f0c2a09d..93ce6efab44 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "ESP er allerede konfigurert" + "already_configured": "ESP er allerede konfigurert", + "already_in_progress": "ESP-konfigurasjon p\u00e5g\u00e5r allerede" }, "error": { "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", "invalid_password": "Ugyldig passord!", - "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, m\u00e5 du [angi en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" + "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst [sett en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" }, "flow_title": "ESPHome: {name}", "step": { @@ -14,8 +15,8 @@ "data": { "password": "Passord" }, - "description": "Vennligst skriv inn passordet du har angitt i din konfigurasjon for {name}.", - "title": "Skriv Inn Passord" + "description": "Vennligst fyll inn passordet du har angitt i din konfigurasjon for {name}.", + "title": "Fyll inn passord" }, "discovery_confirm": { "description": "\u00d8nsker du \u00e5 legge ESPHome noden `{name}` til Home Assistant?", @@ -24,10 +25,10 @@ "user": { "data": { "host": "Vert", - "port": "" + "port": "Port" }, - "description": "Vennligst skriv inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node.", - "title": "ESPHome" + "description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node.", + "title": "" } } } diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index c74b3d57e93..dedde4f1ad8 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP jest ju\u017c skonfigurowane." + "already_configured": "ESP jest ju\u017c skonfigurowane.", + "already_in_progress": "Konfiguracja ESP jest ju\u017c w toku." }, "error": { "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", @@ -12,7 +13,7 @@ "step": { "authenticate": { "data": { - "password": "Has\u0142o" + "password": "[%key_id:common::config_flow::data::password%]" }, "description": "Wprowad\u017a has\u0142o ustawione w konfiguracji dla {name}.", "title": "Wprowad\u017a has\u0142o" @@ -23,11 +24,11 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port" + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]" }, "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.", - "title": "[%key:component::esphome::title%]" + "title": "ESPHome" } } } diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 8361fa371a4..d9407b1c20e 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/esphome/translations/sl.json b/homeassistant/components/esphome/translations/sl.json index 64aa9716f24..15c3577cf9e 100644 --- a/homeassistant/components/esphome/translations/sl.json +++ b/homeassistant/components/esphome/translations/sl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP je \u017ee konfiguriran" + "already_configured": "ESP je \u017ee konfiguriran", + "already_in_progress": "Konfiguracija ESP je \u017ee v teku" }, "error": { "connection_error": "Ne morem se povezati z ESP. Poskrbite, da va\u0161a datoteka YAML vsebuje vrstico \"api:\".", diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 7495204945d..3657af88ce9 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "ESP \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "ESP \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002" }, "error": { "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 570f690307f..2c23eca483f 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) import homeassistant.util.color as color_util from homeassistant.util.color import ( @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EufyLight(discovery_info)], True) -class EufyLight(Light): +class EufyLight(LightEntity): """Representation of a Eufy light.""" def __init__(self, device): diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index cbc09f4101c..586965aa42b 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -3,7 +3,7 @@ import logging import lakeside -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity _LOGGER = logging.getLogger(__name__) @@ -15,7 +15,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EufySwitch(discovery_info)], True) -class EufySwitch(SwitchDevice): +class EufySwitch(SwitchEntity): """Representation of a Eufy switch.""" def __init__(self, device): diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index da9d5b88ae0..95571a825b2 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, - Light, + LightEntity, ) from homeassistant.const import CONF_HOSTS from homeassistant.exceptions import PlatformNotReady @@ -65,7 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(lights) -class EverLightsLight(Light): +class EverLightsLight(LightEntity): """Representation of a Flux light.""" def __init__(self, api, channel, status, effects): diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index b7899afdd7b..c6edb4aa1dc 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -3,7 +3,7 @@ from datetime import datetime as dt import logging from typing import List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -122,7 +122,7 @@ async def async_setup_platform( async_add_entities([controller] + zones, update_before_add=True) -class EvoClimateDevice(EvoDevice, ClimateDevice): +class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" def __init__(self, evo_broker, evo_device) -> None: @@ -142,7 +142,7 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): return self._preset_modes -class EvoZone(EvoChild, EvoClimateDevice): +class EvoZone(EvoChild, EvoClimateEntity): """Base for a Honeywell TCC Zone.""" def __init__(self, evo_broker, evo_device) -> None: @@ -315,7 +315,7 @@ class EvoZone(EvoChild, EvoClimateDevice): self._device_state_attrs[attr] = getattr(self._evo_device, attr) -class EvoController(EvoClimateDevice): +class EvoController(EvoClimateEntity): """Base for a Honeywell TCC Controller/Location. The Controller (aka TCS, temperature control system) is the parent of all the child diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 20aa0710d0d..846c8c09155 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -5,7 +5,7 @@ from typing import List from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -43,7 +43,7 @@ async def async_setup_platform( async_add_entities([new_entity], update_before_add=True) -class EvoDHW(EvoChild, WaterHeaterDevice): +class EvoDHW(EvoChild, WaterHeaterEntity): """Base for a Honeywell TCC DHW controller (aka boiler).""" def __init__(self, evo_broker, evo_device) -> None: diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a395a5da470..4531656f7af 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -45,12 +45,6 @@ ATTR_SPEED_LIST = "speed_list" ATTR_OSCILLATING = "oscillating" ATTR_DIRECTION = "direction" -PROP_TO_ATTR = { - "speed": ATTR_SPEED, - "oscillating": ATTR_OSCILLATING, - "current_direction": ATTR_DIRECTION, -} - @bind_hass def is_on(hass, entity_id: str) -> bool: @@ -166,23 +160,32 @@ class FanEntity(ToggleEntity): """Return the current direction of the fan.""" return None + @property + def oscillating(self): + """Return whether or not the fan is currently oscillating.""" + return None + @property def capability_attributes(self): """Return capability attributes.""" - return {ATTR_SPEED_LIST: self.speed_list} + if self.supported_features & SUPPORT_SET_SPEED: + return {ATTR_SPEED_LIST: self.speed_list} + return {} @property def state_attributes(self) -> dict: """Return optional state attributes.""" data = {} + supported_features = self.supported_features - for prop, attr in PROP_TO_ATTR.items(): - if not hasattr(self, prop): - continue + if supported_features & SUPPORT_DIRECTION: + data[ATTR_DIRECTION] = self.current_direction - value = getattr(self, prop) - if value is not None: - data[attr] = value + if supported_features & SUPPORT_OSCILLATE: + data[ATTR_OSCILLATING] = self.oscillating + + if supported_features & SUPPORT_SET_SPEED: + data[ATTR_SPEED] = self.speed return data diff --git a/homeassistant/components/fan/translations/es-419.json b/homeassistant/components/fan/translations/es-419.json index 6060cff985a..8e9611bdff9 100644 --- a/homeassistant/components/fan/translations/es-419.json +++ b/homeassistant/components/fan/translations/es-419.json @@ -1,5 +1,9 @@ { "device_automation": { + "action_type": { + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 encendido" diff --git a/homeassistant/components/fan/translations/no.json b/homeassistant/components/fan/translations/no.json index 094ca1bc378..c4c425c0eb8 100644 --- a/homeassistant/components/fan/translations/no.json +++ b/homeassistant/components/fan/translations/no.json @@ -13,5 +13,11 @@ "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } }, + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, "title": "Vifte" } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/pl.json b/homeassistant/components/fan/translations/pl.json index b90d6084ca0..fcf925e60ea 100644 --- a/homeassistant/components/fan/translations/pl.json +++ b/homeassistant/components/fan/translations/pl.json @@ -5,8 +5,8 @@ "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "wentylator (entity_name} jest wy\u0142\u0105czony", - "is_on": "wentylator (entity_name} jest w\u0142\u0105czony" + "is_off": "wentylator {entity_name} jest wy\u0142\u0105czony", + "is_on": "wentylator {entity_name} jest w\u0142\u0105czony" }, "trigger_type": { "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index e3a9c09b5d9..a8842f9c401 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -4,7 +4,7 @@ import logging import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.components.ffmpeg import ( CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, @@ -55,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([entity]) -class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice): +class FFmpegBinarySensor(FFmpegBase, BinarySensorEntity): """A binary sensor which use FFmpeg for noise detection.""" def __init__(self, config): diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index fa2d6ceb3c6..251bd1df6a3 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Fibaro binary sensors.""" import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON from . import FIBARO_DEVICES, FibaroDevice @@ -33,7 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): +class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): """Representation of a Fibaro Binary Sensor.""" def __init__(self, fibaro_device): diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 71be289e27b..191185c4a2a 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -1,7 +1,7 @@ """Support for Fibaro thermostats.""" import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -104,7 +104,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class FibaroThermostat(FibaroDevice, ClimateDevice): +class FibaroThermostat(FibaroDevice, ClimateEntity): """Representation of a Fibaro Thermostat.""" def __init__(self, fibaro_device): diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index d2f8094f26d..943df5b5681 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -5,7 +5,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, - CoverDevice, + CoverEntity, ) from . import FIBARO_DEVICES, FibaroDevice @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class FibaroCover(FibaroDevice, CoverDevice): +class FibaroCover(FibaroDevice, CoverEntity): """Representation a Fibaro Cover.""" def __init__(self, fibaro_device): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index d14d9a195d9..f73347cf356 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -11,7 +11,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import CONF_WHITE_VALUE import homeassistant.util.color as color_util @@ -48,7 +48,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class FibaroLight(FibaroDevice, Light): +class FibaroLight(FibaroDevice, LightEntity): """Representation of a Fibaro Light, including dimmable.""" def __init__(self, fibaro_device): diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index b00e5817c9e..a38f642775f 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -1,7 +1,7 @@ """Support for Fibaro switches.""" import logging -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class FibaroSwitch(FibaroDevice, SwitchDevice): +class FibaroSwitch(FibaroDevice, SwitchEntity): """Representation of a Fibaro Switch.""" def __init__(self, fibaro_device): diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 66c283f20ef..7390603c8fa 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -24,6 +24,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.network import get_url from homeassistant.util.json import load_json, save_json _CONFIGURING = {} @@ -180,7 +181,7 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No else: setup_platform(hass, config, add_entities, discovery_info) - start_url = f"{hass.config.api.base_url}{FITBIT_AUTH_CALLBACK_PATH}" + start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -215,7 +216,7 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = f"{hass.config.api.base_url}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -307,7 +308,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET) ) - redirect_uri = f"{hass.config.api.base_url}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, @@ -352,7 +353,7 @@ class FitbitAuthCallbackView(HomeAssistantView): result = None if data.get("code") is not None: - redirect_uri = f"{hass.config.api.base_url}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" try: result = self.oauth.fetch_access_token(data.get("code"), redirect_uri) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 68e13abf8d1..450d09edeb8 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -5,7 +5,7 @@ from typing import List from pyflexit.pyflexit import pyflexit import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, SUPPORT_FAN_MODE, @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([Flexit(hub, modbus_slave, name)], True) -class Flexit(ClimateDevice): +class Flexit(ClimateEntity): """Representation of a Flexit AC unit.""" def __init__(self, hub, modbus_slave, name): diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 55f92e2e5ce..e81f8f2f5b0 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -12,7 +12,7 @@ from pyflic import ( ) import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, @@ -123,7 +123,7 @@ def setup_button(hass, config, add_entities, client, address): add_entities([button]) -class FlicButton(BinarySensorDevice): +class FlicButton(BinarySensorEntity): """Representation of a flic button.""" def __init__(self, hass, client, address, timeout, ignored_click_types): diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py new file mode 100644 index 00000000000..86af47a88bb --- /dev/null +++ b/homeassistant/components/flick_electric/__init__.py @@ -0,0 +1,102 @@ +"""The Flick Electric integration.""" + +from datetime import datetime as dt + +from pyflick import FlickAPI +from pyflick.authentication import AbstractFlickAuth +from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from .const import CONF_TOKEN_EXPIRES_IN, CONF_TOKEN_EXPIRY, DOMAIN + +CONF_ID_TOKEN = "id_token" + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Flick Electric component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Flick Electric from a config entry.""" + auth = HassFlickAuth(hass, entry) + + hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + if await hass.config_entries.async_forward_entry_unload(entry, "sensor"): + hass.data[DOMAIN].pop(entry.entry_id) + return True + + return False + + +class HassFlickAuth(AbstractFlickAuth): + """Implementation of AbstractFlickAuth based on a Home Assistant entity config.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + """Flick authention based on a Home Assistant entity config.""" + super().__init__(aiohttp_client.async_get_clientsession(hass)) + self._entry = entry + self._hass = hass + + async def _get_entry_token(self): + # No token saved, generate one + if ( + CONF_TOKEN_EXPIRY not in self._entry.data + or CONF_ACCESS_TOKEN not in self._entry.data + ): + await self._update_token() + + # Token is expired, generate a new one + if self._entry.data[CONF_TOKEN_EXPIRY] <= dt.now().timestamp(): + await self._update_token() + + return self._entry.data[CONF_ACCESS_TOKEN] + + async def _update_token(self): + token = await self.get_new_token( + username=self._entry.data[CONF_USERNAME], + password=self._entry.data[CONF_PASSWORD], + client_id=self._entry.data.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID), + client_secret=self._entry.data.get( + CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET + ), + ) + + # Reduce expiry by an hour to avoid API being called after expiry + expiry = dt.now().timestamp() + int(token[CONF_TOKEN_EXPIRES_IN] - 3600) + + self._hass.config_entries.async_update_entry( + self._entry, + data={ + **self._entry.data, + CONF_ACCESS_TOKEN: token, + CONF_TOKEN_EXPIRY: expiry, + }, + ) + + async def async_get_access_token(self): + """Get Access Token from HASS Storage.""" + token = await self._get_entry_token() + + return token[CONF_ID_TOKEN] diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py new file mode 100644 index 00000000000..2106a6f8d62 --- /dev/null +++ b/homeassistant/components/flick_electric/config_flow.py @@ -0,0 +1,92 @@ +"""Config Flow for Flick Electric integration.""" +import asyncio +import logging + +import async_timeout +from pyflick.authentication import AuthException, SimpleFlickAuth +from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CLIENT_ID): str, + vol.Optional(CONF_CLIENT_SECRET): str, + } +) + + +class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Flick config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _validate_input(self, user_input): + auth = SimpleFlickAuth( + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + websession=aiohttp_client.async_get_clientsession(self.hass), + client_id=user_input.get(CONF_CLIENT_ID, DEFAULT_CLIENT_ID), + client_secret=user_input.get(CONF_CLIENT_SECRET, DEFAULT_CLIENT_SECRET), + ) + + try: + with async_timeout.timeout(60): + token = await auth.async_get_access_token() + except asyncio.TimeoutError: + raise CannotConnect() + except AuthException: + raise InvalidAuth() + else: + return token is not None + + async def async_step_user(self, user_input): + """Handle gathering login info.""" + errors = {} + if user_input is not None: + try: + await self._validate_input(user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + f"flick_electric_{user_input[CONF_USERNAME]}" + ) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"Flick Electric: {user_input[CONF_USERNAME]}", + data=user_input, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/flick_electric/const.py b/homeassistant/components/flick_electric/const.py new file mode 100644 index 00000000000..e8365f37411 --- /dev/null +++ b/homeassistant/components/flick_electric/const.py @@ -0,0 +1,11 @@ +"""Constants for the Flick Electric integration.""" + +DOMAIN = "flick_electric" + +CONF_TOKEN_EXPIRES_IN = "expires_in" +CONF_TOKEN_EXPIRY = "expires" + +ATTR_START_AT = "start_at" +ATTR_END_AT = "end_at" + +ATTR_COMPONENTS = ["retailer", "ea", "metering", "generation", "admin", "network"] diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json new file mode 100644 index 00000000000..6eb5a2e58f9 --- /dev/null +++ b/homeassistant/components/flick_electric/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "flick_electric", + "name": "Flick Electric", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/flick_electric/", + "requirements": [ + "PyFlick==0.0.2" + ], + "codeowners": [ + "@ZephireNZ" + ] +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py new file mode 100644 index 00000000000..9d441ce7574 --- /dev/null +++ b/homeassistant/components/flick_electric/sensor.py @@ -0,0 +1,83 @@ +"""Support for Flick Electric Pricing data.""" +from datetime import timedelta +import logging + +import async_timeout +from pyflick import FlickAPI, FlickPrice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util.dt import utcnow + +from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT, DOMAIN + +_LOGGER = logging.getLogger(__name__) +_AUTH_URL = "https://api.flick.energy/identity/oauth/token" +_RESOURCE = "https://api.flick.energy/customer/mobile_provider/price" + +SCAN_INTERVAL = timedelta(minutes=5) + +ATTRIBUTION = "Data provided by Flick Electric" +FRIENDLY_NAME = "Flick Power Price" +UNIT_NAME = "cents" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Flick Sensor Setup.""" + api: FlickAPI = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([FlickPricingSensor(api)], True) + + +class FlickPricingSensor(Entity): + """Entity object for Flick Electric sensor.""" + + def __init__(self, api: FlickAPI): + """Entity object for Flick Electric sensor.""" + self._api: FlickAPI = api + self._price: FlickPrice = None + self._attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_FRIENDLY_NAME: FRIENDLY_NAME, + } + + @property + def name(self): + """Return the name of the sensor.""" + return FRIENDLY_NAME + + @property + def state(self): + """Return the state of the sensor.""" + return self._price.price + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return UNIT_NAME + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + async def async_update(self): + """Get the Flick Pricing data from the web service.""" + if self._price and self._price.end_at >= utcnow(): + return # Power price data is still valid + + with async_timeout.timeout(60): + self._price = await self._api.getPricing() + + self._attributes[ATTR_START_AT] = self._price.start_at + self._attributes[ATTR_END_AT] = self._price.end_at + for component in self._price.components: + if component.charge_setter not in ATTR_COMPONENTS: + _LOGGER.warning("Found unknown component: %s", component.charge_setter) + continue + + self._attributes[component.charge_setter] = float(component.value) diff --git a/homeassistant/components/flick_electric/strings.json b/homeassistant/components/flick_electric/strings.json new file mode 100644 index 00000000000..d3d1a9d3bdf --- /dev/null +++ b/homeassistant/components/flick_electric/strings.json @@ -0,0 +1,24 @@ +{ + "title": "Flick Electric", + "config": { + "step": { + "user": { + "title": "Flick Login Credentials", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "client_id": "Client ID (Optional)", + "client_secret": "Client Secret (Optional)" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "That account is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ca.json b/homeassistant/components/flick_electric/translations/ca.json new file mode 100644 index 00000000000..f4eb1bffd45 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "client_id": "ID de client (opcional)", + "client_secret": "Secret de client (opcional)", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Credencials d'inici de sessi\u00f3 de Flick" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json new file mode 100644 index 00000000000..b69e8de8f7c --- /dev/null +++ b/homeassistant/components/flick_electric/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Konto ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/en.json b/homeassistant/components/flick_electric/translations/en.json new file mode 100644 index 00000000000..eb1ce7f296d --- /dev/null +++ b/homeassistant/components/flick_electric/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "That account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID (Optional)", + "client_secret": "Client Secret (Optional)", + "password": "Password", + "username": "Username" + }, + "title": "Flick Login Credentials" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/es.json b/homeassistant/components/flick_electric/translations/es.json new file mode 100644 index 00000000000..d230ba2ba4f --- /dev/null +++ b/homeassistant/components/flick_electric/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esa cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "client_id": "ID de cliente (Opcional)", + "client_secret": "Secreto de Cliente (Opcional)", + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Credenciales de Inicio de Sesi\u00f3n de Flick" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/fi.json b/homeassistant/components/flick_electric/translations/fi.json new file mode 100644 index 00000000000..9be2c5dbf94 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/fi.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "T\u00e4m\u00e4 tili on jo m\u00e4\u00e4ritetty" + }, + "error": { + "cannot_connect": "Yhteyden muodostaminen ep\u00e4onnistui. Yrit\u00e4 uudelleen", + "invalid_auth": "Virheellinen todennus", + "unknown": "Odottamaton virhe" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID (valinnainen)", + "client_secret": "Client Secret (valinnainen)", + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "title": "Flick Login -tunnukset" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/fr.json b/homeassistant/components/flick_electric/translations/fr.json new file mode 100644 index 00000000000..be2135d1e86 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/he.json b/homeassistant/components/flick_electric/translations/he.json new file mode 100644 index 00000000000..85688530711 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05d7\u05e9\u05d1\u05d5\u05df \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "client_secret": "Client Secret (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/it.json b/homeassistant/components/flick_electric/translations/it.json new file mode 100644 index 00000000000..4adec6d5f69 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Questo account \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "client_id": "ID cliente (opzionale)", + "client_secret": "Client Secret (opzionale)", + "password": "Password", + "username": "Nome utente" + }, + "title": "Credenziali di accesso Flick" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ko.json b/homeassistant/components/flick_electric/translations/ko.json new file mode 100644 index 00000000000..65f02204897 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\ud574\ub2f9 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "client_id": "\ud074\ub77c\uc774\uc5b8\ud2b8 ID (\uc120\ud0dd \uc0ac\ud56d)", + "client_secret": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ube44\ubc00\ud0a4 (\uc120\ud0dd \uc0ac\ud56d)", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Flick \ub85c\uadf8\uc778 \uc790\uaca9 \uc99d\uba85" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/lb.json b/homeassistant/components/flick_electric/translations/lb.json new file mode 100644 index 00000000000..4eef13683a2 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID (Optionell)", + "client_secret": "Client Schl\u00ebssel (Optionell)", + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Flick Login Informatiounen" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/no.json b/homeassistant/components/flick_electric/translations/no.json new file mode 100644 index 00000000000..706ab084901 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Denne kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "client_id": "Klient-ID (valgfritt)", + "client_secret": "Klienthemmelighet (valgfritt)", + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Flick p\u00e5loggingsinformasjon" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/pl.json b/homeassistant/components/flick_electric/translations/pl.json new file mode 100644 index 00000000000..ee7f934e787 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ru.json b/homeassistant/components/flick_electric/translations/ru.json new file mode 100644 index 00000000000..c8ea23b3121 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Flick Electric" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/zh-Hant.json b/homeassistant/components/flick_electric/translations/zh-Hant.json new file mode 100644 index 00000000000..0f88c07cdaa --- /dev/null +++ b/homeassistant/components/flick_electric/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "client_id": "\u5ba2\u6236\u7aef ID\uff08\u9078\u9805\uff09", + "client_secret": "\u5ba2\u6236\u7aef\u5bc6\u9470\uff08\u9078\u9805\uff09", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "Flick \u767b\u5165\u6191\u8b49" + } + } + }, + "title": "Flick Electric" +} \ No newline at end of file diff --git a/homeassistant/components/flume/strings.json b/homeassistant/components/flume/strings.json index 50fa03f3e93..039b0d11d72 100644 --- a/homeassistant/components/flume/strings.json +++ b/homeassistant/components/flume/strings.json @@ -10,13 +10,15 @@ "description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at https://portal.flumetech.com/settings#token", "title": "Connect to your Flume Account", "data": { - "username": "Username", + "username": "[%key:common::config_flow::data::username%]", "client_secret": "Client Secret", "client_id": "Client ID", - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } } }, - "abort": { "already_configured": "This account is already configured" } + "abort": { + "already_configured": "This account is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index ecc57551a1f..692c38350a8 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -11,6 +11,8 @@ "step": { "user": { "data": { + "client_id": "Client-ID", + "client_secret": "Client Secret", "password": "Passwort", "username": "Benutzername" }, diff --git a/homeassistant/components/flume/translations/es-419.json b/homeassistant/components/flume/translations/es-419.json new file mode 100644 index 00000000000..026875846c6 --- /dev/null +++ b/homeassistant/components/flume/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "client_id": "Identificaci\u00f3n del cliente", + "client_secret": "Secreto del cliente", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Para acceder a la API personal de Flume, deber\u00e1 solicitar un 'ID de cliente' y un 'Secreto de cliente' en https://portal.flumetech.com/settings#token", + "title": "Con\u00e9ctese a su cuenta Flume" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index a1641a24fc7..a746d793bc4 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "client_id": "ID du client", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/flume/translations/nl.json b/homeassistant/components/flume/translations/nl.json index fe67b5734d1..d176eb13365 100644 --- a/homeassistant/components/flume/translations/nl.json +++ b/homeassistant/components/flume/translations/nl.json @@ -11,9 +11,12 @@ "step": { "user": { "data": { + "client_id": "Client-id", + "client_secret": "Client Secret", "password": "Wachtwoord", "username": "Gebruikersnaam" }, + "description": "Om toegang te krijgen tot de Flume Personal API, moet je een 'Client ID' en 'Client Secret' aanvragen op https://portal.flumetech.com/settings#token", "title": "Verbind met uw Flume account" } } diff --git a/homeassistant/components/flume/translations/no.json b/homeassistant/components/flume/translations/no.json index 1440d3d0477..785f392a255 100644 --- a/homeassistant/components/flume/translations/no.json +++ b/homeassistant/components/flume/translations/no.json @@ -16,7 +16,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "For \u00e5 f\u00e5 tilgang til Flume Personal API, m\u00e5 du be om en \"Klient-ID\" og \"Client Secret\" p\u00e5 https://portal.flumetech.com/settings#token", + "description": "For \u00e5 f\u00e5 tilgang til Flume Personal API, m\u00e5 du be om en 'Client ID' og 'Client Secret' p\u00e5 https://portal.flumetech.com/settings#token", "title": "Koble til Flume-kontoen din" } } diff --git a/homeassistant/components/flume/translations/pl.json b/homeassistant/components/flume/translations/pl.json index 55dfceac11f..1b10ad0b8a0 100644 --- a/homeassistant/components/flume/translations/pl.json +++ b/homeassistant/components/flume/translations/pl.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "To konto jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { "client_id": "Identyfikator klienta", "client_secret": "Has\u0142o klienta", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Aby uzyska\u0107 dost\u0119p do API Flume, musisz poprosi\u0107 o 'ID klienta\u201d i 'klucz tajny klienta' na stronie https://portal.flumetech.com/settings#token", "title": "Po\u0142\u0105cz z kontem Flume" diff --git a/homeassistant/components/flume/translations/sv.json b/homeassistant/components/flume/translations/sv.json new file mode 100644 index 00000000000..f4fdb861fc7 --- /dev/null +++ b/homeassistant/components/flume/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Det h\u00e4r kontot har redan konfigurerats." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "client_id": "Klient ID", + "client_secret": "Klient Nyckel", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "F\u00f6r att f\u00e5 tillg\u00e5ng till Flume Personal API m\u00e5ste du beg\u00e4ra ett 'klient-ID' och 'klienthemlighet' p\u00e5 https://portal.flumetech.com/settings#token", + "title": "Anslut till ditt Flume-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es-419.json b/homeassistant/components/flunearyou/translations/es-419.json new file mode 100644 index 00000000000..9626a509bca --- /dev/null +++ b/homeassistant/components/flunearyou/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Estas coordenadas ya est\u00e1n registradas." + }, + "error": { + "general_error": "Se ha producido un error desconocido." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Monitoree los repotes basados en el usuario y los CDC para un par de coordenadas.", + "title": "Configurar Flu Near You (Gripe cerca de usted)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json index dddcdd64d7b..27789e1b4cf 100644 --- a/homeassistant/components/flunearyou/translations/fr.json +++ b/homeassistant/components/flunearyou/translations/fr.json @@ -11,7 +11,8 @@ "data": { "latitude": "Latitude", "longitude": "Longitude" - } + }, + "title": "Configurer Flu Near You" } } } diff --git a/homeassistant/components/flunearyou/translations/ko.json b/homeassistant/components/flunearyou/translations/ko.json index a41afe3ebc7..5c5e81476ef 100644 --- a/homeassistant/components/flunearyou/translations/ko.json +++ b/homeassistant/components/flunearyou/translations/ko.json @@ -13,7 +13,7 @@ "longitude": "\uacbd\ub3c4" }, "description": "\uc0ac\uc6a9\uc790 \uae30\ubc18 \ub370\uc774\ud130 \ubc0f CDC \ubcf4\uace0\uc11c\uc5d0\uc11c \uc88c\ud45c\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", - "title": "Flu Near You \uad6c\uc131" + "title": "Flu Near You \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json new file mode 100644 index 00000000000..c3f83fc93bf --- /dev/null +++ b/homeassistant/components/flunearyou/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd." + }, + "error": { + "general_error": "Er is een onbekende fout opgetreden." + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "description": "Bewaak op gebruikers gebaseerde en CDC-repots voor een paar co\u00f6rdinaten.", + "title": "Configureer \nFlu Near You" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json index e674d422903..381798ed4ed 100644 --- a/homeassistant/components/flunearyou/translations/pl.json +++ b/homeassistant/components/flunearyou/translations/pl.json @@ -4,14 +4,16 @@ "already_configured": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane." }, "error": { - "general_error": "Nieznany b\u0142\u0105d" + "general_error": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" - } + }, + "description": "Monitoruj repoty oparte na u\u017cytkownikach i CDC dla pary wsp\u00f3\u0142rz\u0119dnych.", + "title": "Skonfiguruj Flu Near You" } } } diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json new file mode 100644 index 00000000000..adcf6008c1e --- /dev/null +++ b/homeassistant/components/flunearyou/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dessa koordinater \u00e4r redan registrerade." + }, + "error": { + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 61bdb9a2862..8a27c99c78d 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( VALID_TRANSITION, is_on, ) -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_LIGHTS, @@ -168,7 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.services.async_register(DOMAIN, service_name, async_update) -class FluxSwitch(SwitchDevice, RestoreEntity): +class FluxSwitch(SwitchEntity, RestoreEntity): """Representation of a Flux switch.""" def __init__( diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 1acd58d8e43..4bfd0c0a26c 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import ATTR_MODE, CONF_DEVICES, CONF_NAME, CONF_PROTOCOL import homeassistant.helpers.config_validation as cv @@ -176,7 +176,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(lights, True) -class FluxLight(Light): +class FluxLight(LightEntity): """Representation of a Flux light.""" def __init__(self, device): diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py new file mode 100644 index 00000000000..45cefc739b9 --- /dev/null +++ b/homeassistant/components/forked_daapd/__init__.py @@ -0,0 +1,34 @@ +"""The forked_daapd component.""" +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN + +from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY + + +async def async_setup(hass, config): + """Set up the forked-daapd component.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up forked-daapd from a config entry by forwarding to platform.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) + ) + return True + + +async def async_unload_entry(hass, entry): + """Remove forked-daapd component.""" + status = await hass.config_entries.async_forward_entry_unload(entry, MP_DOMAIN) + if status and hass.data.get(DOMAIN) and hass.data[DOMAIN].get(entry.entry_id): + hass.data[DOMAIN][entry.entry_id][ + HASS_DATA_UPDATER_KEY + ].websocket_handler.cancel() + for remove_listener in hass.data[DOMAIN][entry.entry_id][ + HASS_DATA_REMOVE_LISTENERS_KEY + ]: + remove_listener() + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + return status diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py new file mode 100644 index 00000000000..07eaaf4c3fe --- /dev/null +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -0,0 +1,187 @@ +"""Config flow to configure forked-daapd devices.""" +import logging + +from pyforked_daapd import ForkedDaapdAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( # pylint:disable=unused-import + CONF_LIBRESPOT_JAVA_PORT, + CONF_MAX_PLAYLISTS, + CONF_TTS_PAUSE_TIME, + CONF_TTS_VOLUME, + DEFAULT_PORT, + DEFAULT_TTS_PAUSE_TIME, + DEFAULT_TTS_VOLUME, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +# Can't use all vol types: https://github.com/home-assistant/core/issues/32819 +DATA_SCHEMA_DICT = { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_PASSWORD, default=""): str, +} + +TEST_CONNECTION_ERROR_DICT = { + "ok": "ok", + "websocket_not_enabled": "websocket_not_enabled", + "wrong_host_or_port": "wrong_host_or_port", + "wrong_password": "wrong_password", + "wrong_server_type": "wrong_server_type", +} + + +class ForkedDaapdOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a forked-daapd options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="options", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TTS_PAUSE_TIME, + default=self.config_entry.options.get( + CONF_TTS_PAUSE_TIME, DEFAULT_TTS_PAUSE_TIME + ), + ): float, + vol.Optional( + CONF_TTS_VOLUME, + default=self.config_entry.options.get( + CONF_TTS_VOLUME, DEFAULT_TTS_VOLUME + ), + ): float, + vol.Optional( + CONF_LIBRESPOT_JAVA_PORT, + default=self.config_entry.options.get( + CONF_LIBRESPOT_JAVA_PORT, 24879 + ), + ): int, + vol.Optional( + CONF_MAX_PLAYLISTS, + default=self.config_entry.options.get(CONF_MAX_PLAYLISTS, 10), + ): int, + } + ), + ) + + +def fill_in_schema_dict(some_input): + """Fill in schema dict defaults from user_input.""" + schema_dict = {} + for field, _type in DATA_SCHEMA_DICT.items(): + if some_input.get(str(field)): + schema_dict[ + vol.Optional(str(field), default=some_input[str(field)]) + ] = _type + else: + schema_dict[field] = _type + return schema_dict + + +class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a forked-daapd config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize.""" + self.discovery_schema = None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return options flow handler.""" + return ForkedDaapdOptionsFlowHandler(config_entry) + + async def validate_input(self, user_input): + """Validate the user input.""" + websession = async_get_clientsession(self.hass) + validate_result = await ForkedDaapdAPI.test_connection( + websession=websession, + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + password=user_input[CONF_PASSWORD], + ) + validate_result[0] = TEST_CONNECTION_ERROR_DICT.get( + validate_result[0], "unknown_error" + ) + return validate_result + + async def async_step_user(self, user_input=None): + """Handle a forked-daapd config flow start. + + Manage device specific parameters. + """ + if user_input is not None: + # check for any entries with same host, abort if found + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + validate_result = await self.validate_input(user_input) + if validate_result[0] == "ok": # success + _LOGGER.debug("Connected successfully. Creating entry") + return self.async_create_entry( + title=validate_result[1], data=user_input + ) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(fill_in_schema_dict(user_input)), + errors={"base": validate_result[0]}, + ) + if self.discovery_schema: # stop at form to allow user to set up manually + return self.async_show_form( + step_id="user", data_schema=self.discovery_schema, errors={} + ) + return self.async_show_form( + step_id="user", data_schema=vol.Schema(DATA_SCHEMA_DICT), errors={} + ) + + async def async_step_zeroconf(self, discovery_info): + """Prepare configuration for a discovered forked-daapd device.""" + if not ( + discovery_info.get("properties") + and int(discovery_info["properties"].get("mtd-version", "0").split(".")[0]) + >= 27 + and discovery_info["properties"].get("Machine Name") + ): + return self.async_abort(reason="not_forked_daapd") + + await self.async_set_unique_id(discovery_info["properties"]["Machine Name"]) + self._abort_if_unique_id_configured() + + # Update title and abort if we already have an entry for this host + for entry in self._async_current_entries(): + if entry.data.get(CONF_HOST) != discovery_info["host"]: + continue + self.hass.config_entries.async_update_entry( + entry, title=discovery_info["properties"]["Machine Name"], + ) + return self.async_abort(reason="already_configured") + + zeroconf_data = { + CONF_HOST: discovery_info["host"], + CONF_PORT: int(discovery_info["port"]), + CONF_NAME: discovery_info["properties"]["Machine Name"], + } + self.discovery_schema = vol.Schema(fill_in_schema_dict(zeroconf_data)) + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update({"title_placeholders": zeroconf_data}) + return await self.async_step_user() diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py new file mode 100644 index 00000000000..29da9f7244e --- /dev/null +++ b/homeassistant/components/forked_daapd/const.py @@ -0,0 +1,85 @@ +"""Const for forked-daapd.""" +from homeassistant.components.media_player.const import ( + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) + +CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server +CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" +CONF_MAX_PLAYLISTS = "max_playlists" +CONF_TTS_PAUSE_TIME = "tts_pause_time" +CONF_TTS_VOLUME = "tts_volume" +DEFAULT_PORT = 3689 +DEFAULT_SERVER_NAME = "My Server" +DEFAULT_TTS_PAUSE_TIME = 1.2 +DEFAULT_TTS_VOLUME = 0.8 +DEFAULT_UNMUTE_VOLUME = 0.6 +DOMAIN = "forked_daapd" # key for hass.data +FD_NAME = "forked-daapd" +HASS_DATA_REMOVE_LISTENERS_KEY = "REMOVE_LISTENERS" +HASS_DATA_UPDATER_KEY = "UPDATER" +KNOWN_PIPES = {"librespot-java"} +PIPE_FUNCTION_MAP = { + "librespot-java": { + "async_media_play": "player_resume", + "async_media_pause": "player_pause", + "async_media_stop": "player_pause", + "async_media_previous_track": "player_prev", + "async_media_next_track": "player_next", + } +} +SIGNAL_ADD_ZONES = "forked-daapd_add_zones {}" +SIGNAL_CONFIG_OPTIONS_UPDATE = "forked-daapd_config_options_update {}" +SIGNAL_UPDATE_DATABASE = "forked-daapd_update_database {}" +SIGNAL_UPDATE_MASTER = "forked-daapd_update_master {}" +SIGNAL_UPDATE_OUTPUTS = "forked-daapd_update_outputs {}" +SIGNAL_UPDATE_PLAYER = "forked-daapd_update_player {}" +SIGNAL_UPDATE_QUEUE = "forked-daapd_update_queue {}" +SOURCE_NAME_CLEAR = "Clear queue" +SOURCE_NAME_DEFAULT = "Default (no pipe)" +STARTUP_DATA = { + "player": { + "state": "stop", + "repeat": "off", + "consume": False, + "shuffle": False, + "volume": 0, + "item_id": 0, + "item_length_ms": 0, + "item_progress_ms": 0, + }, + "queue": {"version": 0, "count": 0, "items": []}, + "outputs": [], +} +SUPPORTED_FEATURES = ( + SUPPORT_PLAY + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_SEEK + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY_MEDIA +) +SUPPORTED_FEATURES_ZONE = ( + SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF +) +TTS_TIMEOUT = 20 # max time to wait between TTS getting sent and starting to play diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json new file mode 100644 index 00000000000..ee57f678601 --- /dev/null +++ b/homeassistant/components/forked_daapd/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "forked_daapd", + "name": "forked-daapd", + "documentation": "https://www.home-assistant.io/integrations/forked-daapd", + "codeowners": ["@uvjustin"], + "requirements": ["pyforked-daapd==0.1.8", "pylibrespot-java==0.1.0"], + "config_flow": true, + "zeroconf": ["_daap._tcp.local."] +} diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py new file mode 100644 index 00000000000..e492aa1b454 --- /dev/null +++ b/homeassistant/components/forked_daapd/media_player.py @@ -0,0 +1,866 @@ +"""This library brings support for forked_daapd to Home Assistant.""" +import asyncio +from collections import defaultdict +import logging + +from pyforked_daapd import ForkedDaapdAPI +from pylibrespot_java import LibrespotJavaAPI + +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.util.dt import utcnow + +from .const import ( + CALLBACK_TIMEOUT, + CONF_LIBRESPOT_JAVA_PORT, + CONF_MAX_PLAYLISTS, + CONF_TTS_PAUSE_TIME, + CONF_TTS_VOLUME, + DEFAULT_TTS_PAUSE_TIME, + DEFAULT_TTS_VOLUME, + DEFAULT_UNMUTE_VOLUME, + DOMAIN, + FD_NAME, + HASS_DATA_REMOVE_LISTENERS_KEY, + HASS_DATA_UPDATER_KEY, + KNOWN_PIPES, + PIPE_FUNCTION_MAP, + SIGNAL_ADD_ZONES, + SIGNAL_CONFIG_OPTIONS_UPDATE, + SIGNAL_UPDATE_DATABASE, + SIGNAL_UPDATE_MASTER, + SIGNAL_UPDATE_OUTPUTS, + SIGNAL_UPDATE_PLAYER, + SIGNAL_UPDATE_QUEUE, + SOURCE_NAME_CLEAR, + SOURCE_NAME_DEFAULT, + STARTUP_DATA, + SUPPORTED_FEATURES, + SUPPORTED_FEATURES_ZONE, + TTS_TIMEOUT, +) + +_LOGGER = logging.getLogger(__name__) + +WS_NOTIFY_EVENT_TYPES = ["player", "outputs", "volume", "options", "queue", "database"] +WEBSOCKET_RECONNECT_TIME = 30 # seconds + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up forked-daapd from a config entry.""" + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + password = config_entry.data[CONF_PASSWORD] + forked_daapd_api = ForkedDaapdAPI( + async_get_clientsession(hass), host, port, password + ) + forked_daapd_master = ForkedDaapdMaster( + clientsession=async_get_clientsession(hass), + api=forked_daapd_api, + ip_address=host, + api_port=port, + api_password=password, + config_entry=config_entry, + ) + + @callback + def async_add_zones(api, outputs): + zone_entities = [] + for output in outputs: + zone_entities.append(ForkedDaapdZone(api, output, config_entry.entry_id)) + async_add_entities(zone_entities, False) + + remove_add_zones_listener = async_dispatcher_connect( + hass, SIGNAL_ADD_ZONES.format(config_entry.entry_id), async_add_zones + ) + remove_entry_listener = config_entry.add_update_listener(update_listener) + + if not hass.data.get(DOMAIN): + hass.data[DOMAIN] = {config_entry.entry_id: {}} + hass.data[DOMAIN][config_entry.entry_id] = { + HASS_DATA_REMOVE_LISTENERS_KEY: [ + remove_add_zones_listener, + remove_entry_listener, + ] + } + async_add_entities([forked_daapd_master], False) + forked_daapd_updater = ForkedDaapdUpdater( + hass, forked_daapd_api, config_entry.entry_id + ) + await forked_daapd_updater.async_init() + hass.data[DOMAIN][config_entry.entry_id][ + HASS_DATA_UPDATER_KEY + ] = forked_daapd_updater + + +async def update_listener(hass, entry): + """Handle options update.""" + async_dispatcher_send( + hass, SIGNAL_CONFIG_OPTIONS_UPDATE.format(entry.entry_id), entry.options + ) + + +class ForkedDaapdZone(MediaPlayerEntity): + """Representation of a forked-daapd output.""" + + def __init__(self, api, output, entry_id): + """Initialize the ForkedDaapd Zone.""" + self._api = api + self._output = output + self._output_id = output["id"] + self._last_volume = DEFAULT_UNMUTE_VOLUME # used for mute/unmute + self._available = True + self._entry_id = entry_id + + async def async_added_to_hass(self): + """Use lifecycle hooks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), + self._async_update_output_callback, + ) + ) + + @callback + def _async_update_output_callback(self, outputs, _event=None): + new_output = next( + (output for output in outputs if output["id"] == self._output_id), None + ) + self._available = bool(new_output) + if self._available: + self._output = new_output + self.async_write_ha_state() + + @property + def unique_id(self): + """Return unique ID.""" + return f"{self._entry_id}-{self._output_id}" + + @property + def should_poll(self) -> bool: + """Entity pushes its state to HA.""" + return False + + async def async_toggle(self): + """Toggle the power on the zone.""" + if self.state == STATE_OFF: + await self.async_turn_on() + else: + await self.async_turn_off() + + @property + def available(self) -> bool: + """Return whether the zone is available.""" + return self._available + + async def async_turn_on(self): + """Enable the output.""" + await self._api.change_output(self._output_id, selected=True) + + async def async_turn_off(self): + """Disable the output.""" + await self._api.change_output(self._output_id, selected=False) + + @property + def name(self): + """Return the name of the zone.""" + return f"{FD_NAME} output ({self._output['name']})" + + @property + def state(self): + """State of the zone.""" + if self._output["selected"]: + return STATE_ON + return STATE_OFF + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._output["volume"] / 100 + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._output["volume"] == 0 + + async def async_mute_volume(self, mute): + """Mute the volume.""" + if mute: + if self.volume_level == 0: + return + self._last_volume = self.volume_level # store volume level to restore later + target_volume = 0 + else: + target_volume = self._last_volume # restore volume level + await self.async_set_volume_level(volume=target_volume) + + async def async_set_volume_level(self, volume): + """Set volume - input range [0,1].""" + await self._api.set_volume(volume=volume * 100, output_id=self._output_id) + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORTED_FEATURES_ZONE + + +class ForkedDaapdMaster(MediaPlayerEntity): + """Representation of the main forked-daapd device.""" + + def __init__( + self, clientsession, api, ip_address, api_port, api_password, config_entry + ): + """Initialize the ForkedDaapd Master Device.""" + self._api = api + self._player = STARTUP_DATA[ + "player" + ] # _player, _outputs, and _queue are loaded straight from api + self._outputs = STARTUP_DATA["outputs"] + self._queue = STARTUP_DATA["queue"] + self._track_info = defaultdict( + str + ) # _track info is found by matching _player data with _queue data + self._last_outputs = [] # used for device on/off + self._last_volume = DEFAULT_UNMUTE_VOLUME + self._player_last_updated = None + self._pipe_control_api = {} + self._ip_address = ( + ip_address # need to save this because pipe control is on same ip + ) + self._tts_pause_time = DEFAULT_TTS_PAUSE_TIME + self._tts_volume = DEFAULT_TTS_VOLUME + self._tts_requested = False + self._tts_queued = False + self._tts_playing_event = asyncio.Event() + self._on_remove = None + self._available = False + self._clientsession = clientsession + self._config_entry = config_entry + self.update_options(config_entry.options) + self._paused_event = asyncio.Event() + self._pause_requested = False + self._sources_uris = {} + self._source = SOURCE_NAME_DEFAULT + self._max_playlists = None + + async def async_added_to_hass(self): + """Use lifecycle hooks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_PLAYER.format(self._config_entry.entry_id), + self._update_player, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_QUEUE.format(self._config_entry.entry_id), + self._update_queue, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_OUTPUTS.format(self._config_entry.entry_id), + self._update_outputs, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_MASTER.format(self._config_entry.entry_id), + self._update_callback, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_CONFIG_OPTIONS_UPDATE.format(self._config_entry.entry_id), + self.update_options, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_DATABASE.format(self._config_entry.entry_id), + self._update_database, + ) + ) + + @callback + def _update_callback(self, available): + """Call update method.""" + self._available = available + self.async_write_ha_state() + + @callback + def update_options(self, options): + """Update forked-daapd server options.""" + if CONF_LIBRESPOT_JAVA_PORT in options: + self._pipe_control_api["librespot-java"] = LibrespotJavaAPI( + self._clientsession, self._ip_address, options[CONF_LIBRESPOT_JAVA_PORT] + ) + if CONF_TTS_PAUSE_TIME in options: + self._tts_pause_time = options[CONF_TTS_PAUSE_TIME] + if CONF_TTS_VOLUME in options: + self._tts_volume = options[CONF_TTS_VOLUME] + if CONF_MAX_PLAYLISTS in options: + # sources not updated until next _update_database call + self._max_playlists = options[CONF_MAX_PLAYLISTS] + + @callback + def _update_player(self, player, event): + self._player = player + self._player_last_updated = utcnow() + self._update_track_info() + if self._tts_queued: + self._tts_playing_event.set() + self._tts_queued = False + if self._pause_requested: + self._paused_event.set() + self._pause_requested = False + event.set() + + @callback + def _update_queue(self, queue, event): + self._queue = queue + if ( + self._tts_requested + and self._queue["count"] == 1 + and self._queue["items"][0]["uri"].find("tts_proxy") != -1 + ): + self._tts_requested = False + self._tts_queued = True + + if ( + self._queue["count"] >= 1 + and self._queue["items"][0]["data_kind"] == "pipe" + and self._queue["items"][0]["title"] in KNOWN_PIPES + ): # if we're playing a pipe, set the source automatically so we can forward controls + self._source = f"{self._queue['items'][0]['title']} (pipe)" + self._update_track_info() + event.set() + + @callback + def _update_outputs(self, outputs, event=None): + if event: # Calling without event is meant for zone, so ignore + self._outputs = outputs + event.set() + + @callback + def _update_database(self, pipes, playlists, event): + self._sources_uris = {SOURCE_NAME_CLEAR: None, SOURCE_NAME_DEFAULT: None} + if pipes: + self._sources_uris.update( + { + f"{pipe['title']} (pipe)": pipe["uri"] + for pipe in pipes + if pipe["title"] in KNOWN_PIPES + } + ) + if playlists: + self._sources_uris.update( + { + f"{playlist['name']} (playlist)": playlist["uri"] + for playlist in playlists[: self._max_playlists] + } + ) + event.set() + + def _update_track_info(self): # run during every player or queue update + try: + self._track_info = next( + track + for track in self._queue["items"] + if track["id"] == self._player["item_id"] + ) + except (StopIteration, TypeError, KeyError): + _LOGGER.debug("Could not get track info") + self._track_info = defaultdict(str) + + @property + def unique_id(self): + """Return unique ID.""" + return self._config_entry.entry_id + + @property + def should_poll(self) -> bool: + """Entity pushes its state to HA.""" + return False + + @property + def available(self) -> bool: + """Return whether the master is available.""" + return self._available + + async def async_turn_on(self): + """Restore the last on outputs state.""" + # restore state + await self._api.set_volume(volume=self._last_volume * 100) + if self._last_outputs: + futures = [] + for output in self._last_outputs: + futures.append( + self._api.change_output( + output["id"], + selected=output["selected"], + volume=output["volume"], + ) + ) + await asyncio.wait(futures) + else: # enable all outputs + await self._api.set_enabled_outputs( + [output["id"] for output in self._outputs] + ) + + async def async_turn_off(self): + """Pause player and store outputs state.""" + await self.async_media_pause() + self._last_outputs = self._outputs + if any([output["selected"] for output in self._outputs]): + await self._api.set_enabled_outputs([]) + + async def async_toggle(self): + """Toggle the power on the device. + + Default media player component method counts idle as off. + We consider idle to be on but just not playing. + """ + if self.state == STATE_OFF: + await self.async_turn_on() + else: + await self.async_turn_off() + + @property + def name(self): + """Return the name of the device.""" + return f"{FD_NAME} server" + + @property + def state(self): + """State of the player.""" + if self._player["state"] == "play": + return STATE_PLAYING + if self._player["state"] == "pause": + return STATE_PAUSED + if not any([output["selected"] for output in self._outputs]): + return STATE_OFF + if self._player["state"] == "stop": # this should catch all remaining cases + return STATE_IDLE + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._player["volume"] / 100 + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._player["volume"] == 0 + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self._player["item_id"] + + @property + def media_content_type(self): + """Content type of current playing media.""" + return self._track_info["media_kind"] + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._player["item_length_ms"] / 1000 + + @property + def media_position(self): + """Position of current playing media in seconds.""" + return self._player["item_progress_ms"] / 1000 + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid.""" + return self._player_last_updated + + @property + def media_title(self): + """Title of current playing media.""" + return self._track_info["title"] + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self._track_info["artist"] + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self._track_info["album"] + + @property + def media_album_artist(self): + """Album artist of current playing media, music track only.""" + return self._track_info["album_artist"] + + @property + def media_track(self): + """Track number of current playing media, music track only.""" + return self._track_info["track_number"] + + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + return self._player["shuffle"] + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORTED_FEATURES + + @property + def source(self): + """Name of the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return [*self._sources_uris] + + async def async_mute_volume(self, mute): + """Mute the volume.""" + if mute: + if self.volume_level == 0: + return + self._last_volume = self.volume_level # store volume level to restore later + target_volume = 0 + else: + target_volume = self._last_volume # restore volume level + await self._api.set_volume(volume=target_volume * 100) + + async def async_set_volume_level(self, volume): + """Set volume - input range [0,1].""" + await self._api.set_volume(volume=volume * 100) + + async def async_media_play(self): + """Start playback.""" + if self._use_pipe_control(): + await self._pipe_call(self._use_pipe_control(), "async_media_play") + else: + await self._api.start_playback() + + async def async_media_pause(self): + """Pause playback.""" + if self._use_pipe_control(): + await self._pipe_call(self._use_pipe_control(), "async_media_pause") + else: + await self._api.pause_playback() + + async def async_media_stop(self): + """Stop playback.""" + if self._use_pipe_control(): + await self._pipe_call(self._use_pipe_control(), "async_media_stop") + else: + await self._api.stop_playback() + + async def async_media_previous_track(self): + """Skip to previous track.""" + if self._use_pipe_control(): + await self._pipe_call( + self._use_pipe_control(), "async_media_previous_track" + ) + else: + await self._api.previous_track() + + async def async_media_next_track(self): + """Skip to next track.""" + if self._use_pipe_control(): + await self._pipe_call(self._use_pipe_control(), "async_media_next_track") + else: + await self._api.next_track() + + async def async_media_seek(self, position): + """Seek to position.""" + await self._api.seek(position_ms=position * 1000) + + async def async_clear_playlist(self): + """Clear playlist.""" + await self._api.clear_queue() + + async def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + await self._api.shuffle(shuffle) + + @property + def media_image_url(self): + """Image url of current playing media.""" + url = self._track_info.get("artwork_url") + if url: + url = self._api.full_url(url) + return url + + async def _save_and_set_tts_volumes(self): + if self.volume_level: # save master volume + self._last_volume = self.volume_level + self._last_outputs = self._outputs + if self._outputs: + await self._api.set_volume(volume=self._tts_volume * 100) + futures = [] + for output in self._outputs: + futures.append( + self._api.change_output( + output["id"], selected=True, volume=self._tts_volume * 100 + ) + ) + await asyncio.wait(futures) + + async def _pause_and_wait_for_callback(self): + """Send pause and wait for the pause callback to be received.""" + self._pause_requested = True + await self.async_media_pause() + try: + await asyncio.wait_for( + self._paused_event.wait(), timeout=CALLBACK_TIMEOUT + ) # wait for paused + except asyncio.TimeoutError: + self._pause_requested = False + self._paused_event.clear() + + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a URI.""" + if media_type == MEDIA_TYPE_MUSIC: + saved_state = self.state # save play state + saved_mute = self.is_volume_muted + sleep_future = asyncio.create_task( + asyncio.sleep(self._tts_pause_time) + ) # start timing now, but not exact because of fd buffer + tts latency + await self._pause_and_wait_for_callback() + await self._save_and_set_tts_volumes() + # save position + saved_song_position = self._player["item_progress_ms"] + saved_queue = ( + self._queue if self._queue["count"] > 0 else None + ) # stash queue + if saved_queue: + saved_queue_position = next( + i + for i, item in enumerate(saved_queue["items"]) + if item["id"] == self._player["item_id"] + ) + self._tts_requested = True + await sleep_future + await self._api.add_to_queue(uris=media_id, playback="start", clear=True) + try: + await asyncio.wait_for( + self._tts_playing_event.wait(), timeout=TTS_TIMEOUT + ) + # we have started TTS, now wait for completion + await asyncio.sleep( + self._queue["items"][0]["length_ms"] + / 1000 # player may not have updated yet so grab length from queue + + self._tts_pause_time + ) + except asyncio.TimeoutError: + self._tts_requested = False + _LOGGER.warning("TTS request timed out") + self._tts_playing_event.clear() + # TTS done, return to normal + await self.async_turn_on() # restore outputs and volumes + if saved_mute: # mute if we were muted + await self.async_mute_volume(True) + if self._use_pipe_control(): # resume pipe + await self._api.add_to_queue( + uris=self._sources_uris[self._source], clear=True + ) + if saved_state == STATE_PLAYING: + await self.async_media_play() + else: # restore stashed queue + if saved_queue: + uris = "" + for item in saved_queue["items"]: + uris += item["uri"] + "," + await self._api.add_to_queue( + uris=uris, + playback="start", + playback_from_position=saved_queue_position, + clear=True, + ) + await self._api.seek(position_ms=saved_song_position) + if saved_state == STATE_PAUSED: + await self.async_media_pause() + elif saved_state != STATE_PLAYING: + await self.async_media_stop() + else: + _LOGGER.debug("Media type '%s' not supported", media_type) + + async def select_source(self, source): + """Change source. + + Source name reflects whether in default mode or pipe mode. + Selecting playlists/clear sets the playlists/clears but ends up in default mode. + """ + if source != self._source: + if ( + self._use_pipe_control() + ): # if pipe was playing, we need to stop it first + await self._pause_and_wait_for_callback() + self._source = source + if not self._use_pipe_control(): # playlist or clear ends up at default + self._source = SOURCE_NAME_DEFAULT + if self._sources_uris.get(source): # load uris for pipes or playlists + await self._api.add_to_queue( + uris=self._sources_uris[source], clear=True + ) + elif source == SOURCE_NAME_CLEAR: # clear playlist + await self._api.clear_queue() + self.async_write_ha_state() + + def _use_pipe_control(self): + """Return which pipe control from KNOWN_PIPES to use.""" + if self._source[-7:] == " (pipe)": + return self._source[:-7] + return "" + + async def _pipe_call(self, pipe_name, base_function_name): + if self._pipe_control_api.get(pipe_name): + return await getattr( + self._pipe_control_api[pipe_name], + PIPE_FUNCTION_MAP[pipe_name][base_function_name], + )() + _LOGGER.warning("No pipe control available for %s", pipe_name) + + +class ForkedDaapdUpdater: + """Manage updates for the forked-daapd device.""" + + def __init__(self, hass, api, entry_id): + """Initialize.""" + self.hass = hass + self._api = api + self.websocket_handler = None + self._all_output_ids = set() + self._entry_id = entry_id + + async def async_init(self): + """Perform async portion of class initialization.""" + server_config = await self._api.get_request("config") + websocket_port = server_config.get("websocket_port") + if websocket_port: + self.websocket_handler = asyncio.create_task( + self._api.start_websocket_handler( + server_config["websocket_port"], + WS_NOTIFY_EVENT_TYPES, + self._update, + WEBSOCKET_RECONNECT_TIME, + self._disconnected_callback, + ) + ) + else: + _LOGGER.error("Invalid websocket port") + + def _disconnected_callback(self): + async_dispatcher_send( + self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), False + ) + async_dispatcher_send( + self.hass, SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), [] + ) + + async def _update(self, update_types): + """Private update method.""" + update_types = set(update_types) + update_events = {} + _LOGGER.debug("Updating %s", update_types) + if ( + "queue" in update_types + ): # update queue, queue before player for async_play_media + queue = await self._api.get_request("queue") + update_events["queue"] = asyncio.Event() + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_QUEUE.format(self._entry_id), + queue, + update_events["queue"], + ) + # order of below don't matter + if not {"outputs", "volume"}.isdisjoint(update_types): # update outputs + outputs = (await self._api.get_request("outputs"))["outputs"] + update_events[ + "outputs" + ] = asyncio.Event() # only for master, zones should ignore + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), + outputs, + update_events["outputs"], + ) + self._add_zones(outputs) + if not {"database"}.isdisjoint(update_types): + pipes, playlists = await asyncio.gather( + self._api.get_pipes(), self._api.get_playlists() + ) + update_events["database"] = asyncio.Event() + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_DATABASE.format(self._entry_id), + pipes, + playlists, + update_events["database"], + ) + if not {"update", "config"}.isdisjoint(update_types): # not supported + _LOGGER.debug("update/config notifications neither requested nor supported") + if not {"player", "options", "volume"}.isdisjoint( + update_types + ): # update player + player = await self._api.get_request("player") + update_events["player"] = asyncio.Event() + if update_events.get("queue"): + await update_events[ + "queue" + ].wait() # make sure queue done before player for async_play_media + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_PLAYER.format(self._entry_id), + player, + update_events["player"], + ) + if update_events: + await asyncio.wait( + [event.wait() for event in update_events.values()] + ) # make sure callbacks done before update + async_dispatcher_send( + self.hass, SIGNAL_UPDATE_MASTER.format(self._entry_id), True + ) + + def _add_zones(self, outputs): + outputs_to_add = [] + for output in outputs: + if output["id"] not in self._all_output_ids: + self._all_output_ids.add(output["id"]) + outputs_to_add.append(output) + if outputs_to_add: + async_dispatcher_send( + self.hass, + SIGNAL_ADD_ZONES.format(self._entry_id), + self._api, + outputs_to_add, + ) diff --git a/homeassistant/components/forked_daapd/strings.json b/homeassistant/components/forked_daapd/strings.json new file mode 100644 index 00000000000..33f9c7b91aa --- /dev/null +++ b/homeassistant/components/forked_daapd/strings.json @@ -0,0 +1,41 @@ +{ + "config": { + "flow_title": "forked-daapd server: {name} ({host})", + "step": { + "user": { + "title": "Set up forked-daapd device", + "data": { + "name": "Friendly name", + "host": "Host", + "port": "API port", + "password": "API password (leave blank if no password)" + } + } + }, + "error": { + "websocket_not_enabled": "forked-daapd server websocket not enabled.", + "wrong_host_or_port": "Unable to connect. Please check host and port.", + "wrong_password": "Incorrect password.", + "wrong_server_type": "The forked-daapd integration requires a forked-daapd server with version >= 27.0.", + "unknown_error": "Unknown error." + }, + "abort": { + "already_configured": "Device is already configured.", + "not_forked_daapd": "Device is not a forked-daapd server." + } + }, + "options": { + "step": { + "init": { + "title": "Configure forked-daapd options", + "description": "Set various options for the forked-daapd integration.", + "data": { + "librespot_java_port": "Port for librespot-java pipe control (if used)", + "max_playlists": "Max number of playlists used as sources", + "tts_volume": "TTS volume (float in range [0,1])", + "tts_pause_time": "Seconds to pause before and after TTS" + } + } + } + } +} diff --git a/homeassistant/components/forked_daapd/translations/en.json b/homeassistant/components/forked_daapd/translations/en.json new file mode 100644 index 00000000000..0c87c6624ff --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured.", + "not_forked_daapd": "Device is not a forked-daapd server." + }, + "error": { + "unknown_error": "Unknown error.", + "websocket_not_enabled": "forked-daapd server websocket not enabled.", + "wrong_host_or_port": "Unable to connect. Please check host and port.", + "wrong_password": "Incorrect password.", + "wrong_server_type": "The forked-daapd integration requires a forked-daapd server with version >= 27.0." + }, + "flow_title": "forked-daapd server: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Friendly name", + "password": "API password (leave blank if no password)", + "port": "API port" + }, + "title": "Set up forked-daapd device" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port for librespot-java pipe control (if used)", + "max_playlists": "Max number of playlists used as sources", + "tts_pause_time": "Seconds to pause before and after TTS", + "tts_volume": "TTS volume (float in range [0,1])" + }, + "description": "Set various options for the forked-daapd integration.", + "title": "Configure forked-daapd options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ru.json b/homeassistant/components/forked_daapd/translations/ru.json new file mode 100644 index 00000000000..448eff8244a --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/ru.json @@ -0,0 +1,41 @@ +{ + "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.", + "not_forked_daapd": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd." + }, + "error": { + "unknown_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", + "wrong_host_or_port": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.", + "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "wrong_server_type": "\u042d\u0442\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd." + }, + "flow_title": "\u0421\u0435\u0440\u0432\u0435\u0440 forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c API (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u043f\u0430\u0440\u043e\u043b\u044f)", + "port": "\u041f\u043e\u0440\u0442 API" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "\u041f\u043e\u0440\u0442 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043a\u0430\u043d\u0430\u043b\u043e\u043c librespot-java (\u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f)", + "max_playlists": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u043e\u0432, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0445 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432", + "tts_pause_time": "\u0412\u0440\u0435\u043c\u044f \u043f\u0430\u0443\u0437\u044b \u0434\u043e \u0438 \u043f\u043e\u0441\u043b\u0435 TTS (\u0441\u0435\u043a.)", + "tts_volume": "\u0413\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u044c TTS (\u0447\u0438\u0441\u043b\u043e \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 \u043e\u0442 0 \u0434\u043e 1)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 forked-daapd.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json new file mode 100644 index 00000000000..b6134c2b720 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_forked_daapd": "\u8a2d\u5099\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" + }, + "error": { + "unknown_error": "\u672a\u77e5\u932f\u8aa4\u3002", + "websocket_not_enabled": "forked-daapd \u4f3a\u670d\u5668 websocket \u672a\u958b\u555f\u3002", + "wrong_host_or_port": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u8207\u901a\u8a0a\u57e0\u3002", + "wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002", + "wrong_server_type": "\u975e forked-daapd \u4f3a\u670d\u5668\u3002" + }, + "flow_title": "forked-daapd \u4f3a\u670d\u5668\uff1a{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u6613\u8a18\u540d\u7a31", + "password": "API \u5bc6\u78bc\uff08\u5047\u5982\u7121\u5bc6\u78bc\uff0c\u8acb\u7559\u7a7a\uff09", + "port": "API \u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a forked-daapd \u8a2d\u5099" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "librespot-java pipe \u63a7\u5236\u6240\u4f7f\u7528\u901a\u8a0a\u57e0\uff08\u5047\u5982\u4f7f\u7528\uff09", + "max_playlists": "\u4f5c\u70ba\u4f86\u6e90\u4e4b\u6700\u5927\u64ad\u653e\u5217\u8868\u6578", + "tts_pause_time": "\u65bc TTS \u524d\u5f8c\u66ab\u505c\u79d2\u6578", + "tts_volume": "TTS \u97f3\u91cf\uff08\u6d6e\u52d5\u7bc4\u570d [0,1]\uff09" + }, + "description": "\u8a2d\u5b9a forked-daapd \u6574\u5408\u9078\u9805\u3002", + "title": "forked-daapd \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fortigate/__init__.py b/homeassistant/components/fortigate/__init__.py index 6de55ae3d65..2dbd7ef45c0 100644 --- a/homeassistant/components/fortigate/__init__.py +++ b/homeassistant/components/fortigate/__init__.py @@ -21,18 +21,21 @@ DOMAIN = "fortigate" DATA_FGT = DOMAIN CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DEVICES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ) - }, + vol.All( + cv.deprecated(DOMAIN, invalidation_version="0.112.0"), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_DEVICES, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/components/fortigate/device_tracker.py b/homeassistant/components/fortigate/device_tracker.py index b51dc6843aa..23df0ee266e 100644 --- a/homeassistant/components/fortigate/device_tracker.py +++ b/homeassistant/components/fortigate/device_tracker.py @@ -60,7 +60,7 @@ class FortigateDeviceScanner(DeviceScanner): await self.async_update_info() return [device.mac for device in self.last_results] - async def get_device_name(self, device): + def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" name = next( (result.hostname for result in self.last_results if result.mac == device), diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index 72265a54558..0fdc4571a1d 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -3,7 +3,10 @@ "step": { "user": { "title": "Freebox", - "data": { "host": "Host", "port": "Port" } + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } }, "link": { "title": "Link Freebox router", @@ -15,6 +18,8 @@ "connection_failed": "Failed to connect, please try again", "unknown": "Unknown error: please retry later" }, - "abort": { "already_configured": "Host already configured" } + "abort": { + "already_configured": "Host already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 9e1011d5d3c..7f8934d9d65 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -4,7 +4,7 @@ from typing import Dict from aiofreepybox.exceptions import InsufficientPermissionsError -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -22,7 +22,7 @@ async def async_setup_entry( async_add_entities([FreeboxWifiSwitch(router)], True) -class FreeboxWifiSwitch(SwitchDevice): +class FreeboxWifiSwitch(SwitchEntity): """Representation of a freebox wifi switch.""" def __init__(self, router: FreeboxRouter) -> None: diff --git a/homeassistant/components/freebox/translations/es-419.json b/homeassistant/components/freebox/translations/es-419.json new file mode 100644 index 00000000000..01583551453 --- /dev/null +++ b/homeassistant/components/freebox/translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Host ya configurado" + }, + "error": { + "connection_failed": "No se pudo conectar, intente nuevamente", + "register_failed": "No se pudo registrar, intente de nuevo", + "unknown": "Error desconocido: vuelva a intentarlo m\u00e1s tarde" + }, + "step": { + "link": { + "description": "Haga clic en \"Enviar\", luego toque la flecha derecha en el enrutador para registrar Freebox con Home Assistant. \n\n![Location of button on the router](/static/images/config_freebox.png)", + "title": "Enlazar enrutador Freebox" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/fr.json b/homeassistant/components/freebox/translations/fr.json index 3854823843b..3e3c2f12c97 100644 --- a/homeassistant/components/freebox/translations/fr.json +++ b/homeassistant/components/freebox/translations/fr.json @@ -10,7 +10,8 @@ }, "step": { "link": { - "description": "Cliquez sur \u00abSoumettre\u00bb, puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer Freebox avec Home Assistant. \n\n ! [Emplacement du bouton sur le routeur](/static/images/config_freebox.png)" + "description": "Cliquez sur \u00abSoumettre\u00bb, puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer Freebox avec Home Assistant. \n\n ! [Emplacement du bouton sur le routeur](/static/images/config_freebox.png)", + "title": "Lien routeur Freebox" }, "user": { "data": { diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json new file mode 100644 index 00000000000..2b5265bc671 --- /dev/null +++ b/homeassistant/components/freebox/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json index 9c86ce6a775..eca391cbd9f 100644 --- a/homeassistant/components/freebox/translations/ko.json +++ b/homeassistant/components/freebox/translations/ko.json @@ -11,7 +11,7 @@ "step": { "link": { "description": "\"Submit\" \uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant \uc5d0 Freebox \ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n![\ub77c\uc6b0\ud130\uc758 \ubc84\ud2bc \uc704\uce58](/static/images/config_freebox.png)", - "title": "Freebox \ub77c\uc6b0\ud130 \uc5f0\uacb0" + "title": "Freebox \ub77c\uc6b0\ud130 \uc5f0\uacb0\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/freebox/translations/nl.json b/homeassistant/components/freebox/translations/nl.json new file mode 100644 index 00000000000..62c69997e17 --- /dev/null +++ b/homeassistant/components/freebox/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "connection_failed": "Verbinding mislukt, probeer het opnieuw", + "register_failed": "Registratie is mislukt, probeer het opnieuw", + "unknown": "Onbekende fout: probeer het later nog eens" + }, + "step": { + "link": { + "description": "Klik op \"Verzenden\" en tik vervolgens op de rechterpijl op de router om Freebox te registreren bij Home Assistant. \n\n ! [Locatie van knop op de router] (/ static / images / config_freebox.png)", + "title": "Freebox-router koppelen" + }, + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/no.json b/homeassistant/components/freebox/translations/no.json index 0ec9bf70ecd..36152c9a815 100644 --- a/homeassistant/components/freebox/translations/no.json +++ b/homeassistant/components/freebox/translations/no.json @@ -16,7 +16,7 @@ "user": { "data": { "host": "Vert", - "port": "" + "port": "Port" }, "title": "" } diff --git a/homeassistant/components/freebox/translations/pl.json b/homeassistant/components/freebox/translations/pl.json index 9df523af0fb..770194e338e 100644 --- a/homeassistant/components/freebox/translations/pl.json +++ b/homeassistant/components/freebox/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Host jest ju\u017c skonfigurowany." + "already_configured": "[%key_id:common::config_flow::data::host%]" }, "error": { "connection_failed": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", @@ -15,8 +15,8 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port" + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]" }, "title": "Freebox" } diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json new file mode 100644 index 00000000000..6c6cc5c64ec --- /dev/null +++ b/homeassistant/components/freebox/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "connection_failed": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen", + "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index c0893b93316..7db216c32e1 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Fritzbox binary sensors.""" import requests -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_DEVICES from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER @@ -21,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class FritzboxBinarySensor(BinarySensorDevice): +class FritzboxBinarySensor(BinarySensorEntity): """Representation of a binary Fritzbox device.""" def __init__(self, device, fritz): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 1c95d918ab8..4abe82776a9 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,7 +1,7 @@ """Support for AVM Fritz!Box smarthome thermostate devices.""" import requests -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, HVAC_MODE_HEAT, @@ -61,7 +61,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class FritzboxThermostat(ClimateDevice): +class FritzboxThermostat(ClimateEntity): """The thermostat class for Fritzbox smarthome thermostates.""" def __init__(self, device, fritz): diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 227aeedf84d..3b287fa38a5 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -5,16 +5,16 @@ "user": { "description": "Enter your AVM FRITZ!Box information.", "data": { - "host": "Host or IP address", - "username": "Username", - "password": "Password" + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, "confirm": { "description": "Do you want to set up {name}?", "data": { - "username": "Username", - "password": "Password" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -28,4 +28,4 @@ "auth_failed": "Username and/or password are incorrect." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 6f98667304b..b179464182f 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,7 +1,7 @@ """Support for AVM Fritz!Box smarthome switch devices.""" import requests -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICES, @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class FritzboxSwitch(SwitchDevice): +class FritzboxSwitch(SwitchEntity): """The switch class for Fritzbox switches.""" def __init__(self, device, fritz): diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index 44eac165110..70c2173c641 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Aquest AVM FRITZ!Box ja est\u00e0 configurat.", "already_in_progress": "La configuraci\u00f3 de l'AVM FRITZ!Box ja est\u00e0 en curs.", - "not_found": "No s'ha trobat cap AVM FRITZ!Box compatible a la xarxa." + "not_found": "No s'ha trobat cap AVM FRITZ!Box compatible a la xarxa.", + "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home." }, "error": { "auth_failed": "Nom d'usuari i/o contrasenya incorrectes." diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 5f16553c64c..1a2e046d933 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Diese AVM FRITZ! Box ist bereits konfiguriert.", "already_in_progress": "Die Konfiguration der AVM FRITZ! Box ist bereits in Bearbeitung.", - "not_found": "Keine unterst\u00fctzte AVM FRITZ! Box im Netzwerk gefunden." + "not_found": "Keine unterst\u00fctzte AVM FRITZ! Box im Netzwerk gefunden.", + "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern." }, "error": { "auth_failed": "Benutzername und/oder Passwort sind falsch." diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json index d5d889a9083..4f43626c667 100644 --- a/homeassistant/components/fritzbox/translations/en.json +++ b/homeassistant/components/fritzbox/translations/en.json @@ -21,7 +21,7 @@ }, "user": { "data": { - "host": "Host or IP address", + "host": "Host", "password": "Password", "username": "Username" }, diff --git a/homeassistant/components/fritzbox/translations/es-419.json b/homeassistant/components/fritzbox/translations/es-419.json new file mode 100644 index 00000000000..4e8003a06d8 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/es-419.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n de AVM FRITZ!Box ya est\u00e1 en progreso.", + "not_found": "No se encontr\u00f3 ning\u00fan AVM FRITZ!Box compatible en la red.", + "not_supported": "Conectado a AVM FRITZ!Box pero no puede controlar dispositivos Smart Home." + }, + "error": { + "auth_failed": "El nombre de usuario y/o la contrase\u00f1a son incorrectos." + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "\u00bfDesea configurar {name}?", + "title": "AVM FRITZ!Box" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Ingrese la informaci\u00f3n de su AVM FRITZ!Box.", + "title": "AVM FRITZ!Box" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/fi.json b/homeassistant/components/fritzbox/translations/fi.json new file mode 100644 index 00000000000..bb4fb818a67 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/fi.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + } + }, + "user": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "title": "AVM FRITZ!Box" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 5072d62374b..97f8d97430d 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -1,22 +1,32 @@ { "config": { + "abort": { + "already_configured": "Cette AVM FRITZ!Box est d\u00e9j\u00e0 configur\u00e9e.", + "already_in_progress": "Une configuration d'AVM FRITZ!Box est d\u00e9j\u00e0 en cours.", + "not_found": "Aucune AVM FRITZ!Box support\u00e9e trouv\u00e9e sur le r\u00e9seau.", + "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home." + }, "error": { "auth_failed": "Le nom d'utilisateur et / ou le mot de passe sont incorrects." }, + "flow_title": "AVM FRITZ!Box : {name}", "step": { "confirm": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Voulez-vous configurer {name} ?" + "description": "Voulez-vous configurer {name} ?", + "title": "AVM FRITZ!Box" }, "user": { "data": { "host": "H\u00f4te ou adresse IP", "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "description": "Entrez les informations de votre AVM FRITZ!Box.", + "title": "AVM FRITZ!Box" } } } diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json new file mode 100644 index 00000000000..09397b70a7d --- /dev/null +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index de0f1e0e918..f51868956bc 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Questo AVM FRITZ!Box \u00e8 gi\u00e0 configurato.", "already_in_progress": "La configurazione di AVM FRITZ!Box \u00e8 gi\u00e0 in corso.", - "not_found": "Nessun AVM FRITZ!Box supportato trovato sulla rete." + "not_found": "Nessun AVM FRITZ!Box supportato trovato sulla rete.", + "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home." }, "error": { "auth_failed": "Nome utente e/o password non sono corretti." diff --git a/homeassistant/components/fritzbox/translations/ko.json b/homeassistant/components/fritzbox/translations/ko.json new file mode 100644 index 00000000000..395749b2b9f --- /dev/null +++ b/homeassistant/components/fritzbox/translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 AVM FRITZ!Box \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_in_progress": "AVM FRITZ!Box \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "not_found": "\uc9c0\uc6d0\ub418\ub294 AVM FRITZ!Box \uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "not_supported": "AVM FRITZ!Box \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "auth_failed": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "AVM FRITZ!Box" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "AVM FRITZ!Box \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "AVM FRITZ!Box" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/lb.json b/homeassistant/components/fritzbox/translations/lb.json index 4f82a03859c..d9126243577 100644 --- a/homeassistant/components/fritzbox/translations/lb.json +++ b/homeassistant/components/fritzbox/translations/lb.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "D\u00ebs AVM FRITZ!Box ass scho konfigur\u00e9iert", "already_in_progress": "AVM FRITZ!Box Konfiguratioun ass schonn am gaang.", - "not_found": "Keng \u00ebnnerst\u00ebtzte AVM FRITZ!Box am Netzwierk fonnt." + "not_found": "Keng \u00ebnnerst\u00ebtzte AVM FRITZ!Box am Netzwierk fonnt.", + "not_supported": "Mat der AVM FRITZ!Box verbonnen mee sie kann keng Smart Home Apparater kontroll\u00e9ieren." }, "error": { "auth_failed": "Benotzernumm an/oder Passwuert inkorrekt" diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index 3d8c94ab5b6..d6391d16803 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Deze AVM FRITZ!Box is al geconfigureerd.", "already_in_progress": "AVM FRITZ!Box configuratie is al bezig.", - "not_found": "Geen ondersteunde AVM FRITZ!Box gevonden op het netwerk." + "not_found": "Geen ondersteunde AVM FRITZ!Box gevonden op het netwerk.", + "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen." }, "error": { "auth_failed": "Ongeldige gebruikersnaam of wachtwoord" @@ -20,7 +21,7 @@ }, "user": { "data": { - "host": "Host- of IP-adres", + "host": "Host of IP-adres", "password": "Wachtwoord", "username": "Gebruikersnaam" }, diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index b6a7fe0641f..a21067bb112 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -3,12 +3,13 @@ "abort": { "already_configured": "Denne AVM FRITZ!Box er allerede konfigurert.", "already_in_progress": "AVM FRITZ!Box-konfigurasjon p\u00e5g\u00e5r allerede.", - "not_found": "Ingen st\u00f8ttet AVM FRITZ!Box funnet p\u00e5 nettverket." + "not_found": "Ingen st\u00f8ttet AVM FRITZ!Box funnet p\u00e5 nettverket.", + "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter." }, "error": { "auth_failed": "Brukernavn og/eller passord er feil." }, - "flow_title": "", + "flow_title": "AVM FRITZ!Box: {name}", "step": { "confirm": { "data": { @@ -24,7 +25,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Skriv inn AVM FRITZ!Box informasjonen.", + "description": "Fyll inn AVM FRITZ!Box informasjonen.", "title": "" } } diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json index 8c9d58c3c42..be545f40b1a 100644 --- a/homeassistant/components/fritzbox/translations/pl.json +++ b/homeassistant/components/fritzbox/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ten AVM FRITZ!Box jest ju\u017c skonfigurowany.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "already_in_progress": "Konfiguracja AVM FRITZ!Box jest ju\u017c w toku.", "not_found": "W sieci nie znaleziono obs\u0142ugiwanego urz\u0105dzenia AVM FRITZ!Box.", "not_supported": "Po\u0142\u0105czony z AVM FRITZ! Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home." @@ -13,17 +13,17 @@ "step": { "confirm": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Czy chcesz skonfigurowa\u0107 {name}?", "title": "AVM FRITZ! Box" }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "host": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Wprowad\u017a informacje o urz\u0105dzeniu AVM FRITZ! Box.", "title": "AVM FRITZ! Box" diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 9f19b4d3faa..ce8f88bc4e4 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -3,7 +3,8 @@ "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.", "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.", - "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." + "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e." }, "error": { "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." @@ -20,11 +21,11 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0412\u0430\u0448\u0435\u043c AVM FRITZ! Box.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AVM FRITZ!Box.", "title": "AVM FRITZ!Box" } } diff --git a/homeassistant/components/fritzbox/translations/sl.json b/homeassistant/components/fritzbox/translations/sl.json new file mode 100644 index 00000000000..484ab28b265 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/sl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Ta AVM FRITZ! Box je \u017ee konfiguriran.", + "already_in_progress": "Konfiguracija AVM FRITZ! Box je \u017ee v teku.", + "not_found": "V omre\u017eju ni bilo najdenih nobenih podprtih naprav AVM Fritz!Box.", + "not_supported": "Povezani z AVM FRITZ! Box, vendar ne morete upravljati pametnih naprav." + }, + "error": { + "auth_failed": "Uporabni\u0161ko ime in/ali geslo sta napa\u010dna." + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Ali \u017eelite nastaviti {name} ?", + "title": "AVM FRITZ!Box" + }, + "user": { + "data": { + "host": "Gostitelj ali IP naslov", + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite svoje podatke za AVM FRITZ! Box.", + "title": "AVM FRITZ!Box" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json new file mode 100644 index 00000000000..1ed4e4fc3d8 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "auth_failed": "Anv\u00e4ndarnamn och/eller l\u00f6senord \u00e4r fel." + }, + "step": { + "confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Do vill du konfigurera {name}?" + }, + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index 3bc06734e83..f517a667d97 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u6b64 AVM FRITZ!Box \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "AVM FRITZ!Box \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "not_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u652f\u63f4\u7684 AVM FRITZ!Box\u3002" + "not_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u652f\u63f4\u7684 AVM FRITZ!Box\u3002", + "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u8a2d\u5099\u3002" }, "error": { "auth_failed": "\u4f7f\u7528\u8005\u53ca/\u6216\u5bc6\u78bc\u932f\u8aa4\u3002" @@ -20,7 +21,7 @@ }, "user": { "data": { - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 0ed1697bf42..563e71c2eec 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200427.2"], + "requirements": ["home-assistant-frontend==20200519.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 93e96d6e967..852528fb3a5 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -5,7 +5,7 @@ from afsapi import AFSAPI import requests import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -94,7 +94,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return False -class AFSAPIDevice(MediaPlayerDevice): +class AFSAPIDevice(MediaPlayerEntity): """Representation of a Frontier Silicon device on the network.""" def __init__(self, device_url, password, name): diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index 4da3bfd5bc3..2bccd38688a 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -64,7 +64,7 @@ def to_hass_level(level): return int((level * 255) / 100) -class FutureNowLight(Light): +class FutureNowLight(LightEntity): """Representation of an FutureNow light.""" def __init__(self, device): diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index a1f324be1ff..34a9a13b8d9 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_COVERS, @@ -74,7 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(covers) -class GaradgetCover(CoverDevice): +class GaradgetCover(CoverEntity): """Representation of a Garadget cover.""" def __init__(self, hass, args): diff --git a/homeassistant/components/garmin_connect/strings.json b/homeassistant/components/garmin_connect/strings.json index 1f14d91e04a..6aac2686a5c 100644 --- a/homeassistant/components/garmin_connect/strings.json +++ b/homeassistant/components/garmin_connect/strings.json @@ -1,6 +1,8 @@ { "config": { - "abort": { "already_configured": "This account is already configured." }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "cannot_connect": "Failed to connect, please try again.", "invalid_auth": "Invalid authentication.", @@ -9,10 +11,13 @@ }, "step": { "user": { - "data": { "password": "Password", "username": "Username" }, + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, "description": "Enter your credentials.", "title": "Garmin Connect" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/ca.json b/homeassistant/components/garmin_connect/translations/ca.json index d128e1d2e25..34d7273ef96 100644 --- a/homeassistant/components/garmin_connect/translations/ca.json +++ b/homeassistant/components/garmin_connect/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Aquest compte ja est\u00e0 configurat." + "already_configured": "[%key::common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar.", diff --git a/homeassistant/components/garmin_connect/translations/en.json b/homeassistant/components/garmin_connect/translations/en.json index 52ae403cca3..3b3b2fcb865 100644 --- a/homeassistant/components/garmin_connect/translations/en.json +++ b/homeassistant/components/garmin_connect/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "This account is already configured." + "already_configured": "Account is already configured" }, "error": { "cannot_connect": "Failed to connect, please try again.", diff --git a/homeassistant/components/garmin_connect/translations/es-419.json b/homeassistant/components/garmin_connect/translations/es-419.json index 6e20b4cd2cc..42263ce0780 100644 --- a/homeassistant/components/garmin_connect/translations/es-419.json +++ b/homeassistant/components/garmin_connect/translations/es-419.json @@ -15,7 +15,8 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Ingrese sus credenciales." + "description": "Ingrese sus credenciales.", + "title": "Garmin Connect" } } } diff --git a/homeassistant/components/garmin_connect/translations/it.json b/homeassistant/components/garmin_connect/translations/it.json index 70f4bf84017..62937ed2ab1 100644 --- a/homeassistant/components/garmin_connect/translations/it.json +++ b/homeassistant/components/garmin_connect/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Questo account \u00e8 gi\u00e0 configurato." + "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "error": { "cannot_connect": "Impossibile connettersi, si prega di riprovare.", diff --git a/homeassistant/components/garmin_connect/translations/ko.json b/homeassistant/components/garmin_connect/translations/ko.json index 4727a21db6a..fee07e579fe 100644 --- a/homeassistant/components/garmin_connect/translations/ko.json +++ b/homeassistant/components/garmin_connect/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/garmin_connect/translations/no.json b/homeassistant/components/garmin_connect/translations/no.json index ae678abfe05..28732d8c194 100644 --- a/homeassistant/components/garmin_connect/translations/no.json +++ b/homeassistant/components/garmin_connect/translations/no.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Denne kontoen er allerede konfigurert." - }, "error": { "cannot_connect": "Kunne ikke koble til, pr\u00f8v igjen.", "invalid_auth": "Ugyldig godkjenning.", @@ -15,7 +12,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Angi brukeropplysninger.", + "description": "Fyll inn legitimasjonen din.", "title": "" } } diff --git a/homeassistant/components/garmin_connect/translations/pl.json b/homeassistant/components/garmin_connect/translations/pl.json index e144ac994f7..2ecd1114799 100644 --- a/homeassistant/components/garmin_connect/translations/pl.json +++ b/homeassistant/components/garmin_connect/translations/pl.json @@ -5,15 +5,15 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Garmin Connect" diff --git a/homeassistant/components/garmin_connect/translations/zh-Hant.json b/homeassistant/components/garmin_connect/translations/zh-Hant.json index f13f62abbed..c7351932734 100644 --- a/homeassistant/components/garmin_connect/translations/zh-Hant.json +++ b/homeassistant/components/garmin_connect/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u6b64\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index a2f8ba4a0a2..43ceb75e449 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -1,7 +1,7 @@ """Support for binary sensor using GC100.""" import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors, True) -class GC100BinarySensor(BinarySensorDevice): +class GC100BinarySensor(BinarySensorEntity): """Representation of a binary sensor from GC100.""" def __init__(self, name, port_addr, gc100): diff --git a/homeassistant/components/gdacs/translations/es-419.json b/homeassistant/components/gdacs/translations/es-419.json new file mode 100644 index 00000000000..6b6999a196e --- /dev/null +++ b/homeassistant/components/gdacs/translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index f153a47e2a4..d7889513402 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_PRESET_MODE, CURRENT_HVAC_COOL, @@ -127,7 +127,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class GenericThermostat(ClimateDevice, RestoreEntity): +class GenericThermostat(ClimateEntity, RestoreEntity): """Representation of a Generic Thermostat device.""" def __init__( diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 33458d049a2..d935192f97d 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,5 +1,5 @@ """Support for Genius Hub binary_sensor devices.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import DOMAIN, GeniusDevice @@ -25,7 +25,7 @@ async def async_setup_platform( async_add_entities(switches, update_before_add=True) -class GeniusBinarySensor(GeniusDevice, BinarySensorDevice): +class GeniusBinarySensor(GeniusDevice, BinarySensorEntity): """Representation of a Genius Hub binary_sensor.""" def __init__(self, broker, device, state_attr) -> None: diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 2221b8706c8..70d08dc2d1f 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,7 +1,7 @@ """Support for Genius Hub climate devices.""" from typing import List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -45,7 +45,7 @@ async def async_setup_platform( ) -class GeniusClimateZone(GeniusHeatingZone, ClimateDevice): +class GeniusClimateZone(GeniusHeatingZone, ClimateEntity): """Representation of a Genius Hub climate device.""" def __init__(self, broker, zone) -> None: diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index b73c9a89041..e73468321bd 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -1,5 +1,5 @@ """Support for Genius Hub switch/outlet devices.""" -from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchDevice +from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import DOMAIN, GeniusZone @@ -27,7 +27,7 @@ async def async_setup_platform( ) -class GeniusSwitch(GeniusZone, SwitchDevice): +class GeniusSwitch(GeniusZone, SwitchEntity): """Representation of a Genius Hub switch.""" @property diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index e7e3278eaf6..51fdce4a6d7 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -4,7 +4,7 @@ from typing import List from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -49,7 +49,7 @@ async def async_setup_platform( ) -class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterDevice): +class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterEntity): """Representation of a Genius Hub water_heater device.""" def __init__(self, broker, zone) -> None: diff --git a/homeassistant/components/geofency/translations/ko.json b/homeassistant/components/geofency/translations/ko.json index f1ed98f50cd..d0d0fde2efd 100644 --- a/homeassistant/components/geofency/translations/ko.json +++ b/homeassistant/components/geofency/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "Geofency Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Geofency Webhook \uc124\uc815" + "description": "Geofency \uc6f9 \ud6c5\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Geofency \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/geofency/translations/no.json b/homeassistant/components/geofency/translations/no.json index ea9e1827b63..8e66cab4c9c 100644 --- a/homeassistant/components/geofency/translations/no.json +++ b/homeassistant/components/geofency/translations/no.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Er du sikker p\u00e5 at du vil konfigurere Geofency Webhook?", + "description": "Er du sikker p\u00e5 at du vil sette opp Geofency Webhook?", "title": "Sett opp Geofency Webhook" } } diff --git a/homeassistant/components/geonetnz_quakes/translations/es-419.json b/homeassistant/components/geonetnz_quakes/translations/es-419.json new file mode 100644 index 00000000000..7afffd7bb97 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/no.json b/homeassistant/components/geonetnz_quakes/translations/no.json index b14e0ded378..fc3b339d807 100644 --- a/homeassistant/components/geonetnz_quakes/translations/no.json +++ b/homeassistant/components/geonetnz_quakes/translations/no.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "mmi": "MMI", - "radius": "Radius" + "mmi": "", + "radius": "" }, "title": "Fyll ut filterdetaljene." } diff --git a/homeassistant/components/geonetnz_quakes/translations/sv.json b/homeassistant/components/geonetnz_quakes/translations/sv.json index b1040e9bc23..feb654c267c 100644 --- a/homeassistant/components/geonetnz_quakes/translations/sv.json +++ b/homeassistant/components/geonetnz_quakes/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_volcano/translations/es-419.json b/homeassistant/components/geonetnz_volcano/translations/es-419.json new file mode 100644 index 00000000000..c26033e1861 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/no.json b/homeassistant/components/geonetnz_volcano/translations/no.json index 17ce4a32b40..646afcc1d16 100644 --- a/homeassistant/components/geonetnz_volcano/translations/no.json +++ b/homeassistant/components/geonetnz_volcano/translations/no.json @@ -6,9 +6,9 @@ "step": { "user": { "data": { - "radius": "Radius" + "radius": "" }, - "title": "Fyll ut filterdetaljene." + "title": "Fyll inn dine filterdetaljer." } } } diff --git a/homeassistant/components/gios/translations/ca.json b/homeassistant/components/gios/translations/ca.json index 1e451432be7..29703281b08 100644 --- a/homeassistant/components/gios/translations/ca.json +++ b/homeassistant/components/gios/translations/ca.json @@ -14,7 +14,7 @@ "name": "Nom de la integraci\u00f3", "station_id": "ID de l'estaci\u00f3 de mesura" }, - "description": "Integraci\u00f3 de mesura de qualitat de l\u2019aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", + "description": "Integraci\u00f3 de mesura de qualitat de l'aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/es-419.json b/homeassistant/components/gios/translations/es-419.json index 53439a7ab7b..848247bdf75 100644 --- a/homeassistant/components/gios/translations/es-419.json +++ b/homeassistant/components/gios/translations/es-419.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "La integraci\u00f3n de GIO\u015a para esta estaci\u00f3n de medici\u00f3n ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se puede conectar al servidor GIO\u015a.", + "invalid_sensors_data": "Datos de sensores no v\u00e1lidos para esta estaci\u00f3n de medici\u00f3n.", + "wrong_station_id": "La identificaci\u00f3n de la estaci\u00f3n de medici\u00f3n no es correcta." + }, "step": { "user": { "data": { - "name": "Nombre de la integraci\u00f3n" - } + "name": "Nombre de la integraci\u00f3n", + "station_id": "Identificaci\u00f3n de la estaci\u00f3n de medici\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n Ambiental de Polonia). Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } } diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json index 93c78b33db9..784b75c9ee5 100644 --- a/homeassistant/components/gios/translations/no.json +++ b/homeassistant/components/gios/translations/no.json @@ -14,8 +14,8 @@ "name": "Navn p\u00e5 integrasjon", "station_id": "ID til m\u00e5lestasjon" }, - "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrering. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios", - "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrasjon. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios", + "title": "" } } } diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index ae8ab0357f3..68f59c50f98 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -5,10 +5,10 @@ "title": "Setup Glances", "data": { "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port", + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", "version": "Glances API Version (2 or 3)", "ssl": "Use SSL/TLS to connect to the Glances system", "verify_ssl": "Verify the certification of the system" @@ -19,14 +19,18 @@ "cannot_connect": "Unable to connect to host", "wrong_version": "Version not supported (2 or 3 only)" }, - "abort": { "already_configured": "Host is already configured." } + "abort": { + "already_configured": "Host is already configured." + } }, "options": { "step": { "init": { "description": "Configure options for Glances", - "data": { "scan_interval": "Update frequency" } + "data": { + "scan_interval": "Update frequency" + } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/ca.json b/homeassistant/components/glances/translations/ca.json index dd9d151296f..7da63024f8b 100644 --- a/homeassistant/components/glances/translations/ca.json +++ b/homeassistant/components/glances/translations/ca.json @@ -27,7 +27,7 @@ "step": { "init": { "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" }, "description": "Opcions de configuraci\u00f3 de Glances" } diff --git a/homeassistant/components/glances/translations/es-419.json b/homeassistant/components/glances/translations/es-419.json index 6debc6da6c1..5e060b20d47 100644 --- a/homeassistant/components/glances/translations/es-419.json +++ b/homeassistant/components/glances/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, "error": { "cannot_connect": "No se puede conectar al host", "wrong_version": "Versi\u00f3n no compatible (2 o 3 solamente)" @@ -7,13 +10,16 @@ "step": { "user": { "data": { + "host": "Host", "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", + "ssl": "Use SSL/TLS para conectarse al sistema Glances", "username": "Nombre de usuario", "verify_ssl": "Verificar la certificaci\u00f3n del sistema", "version": "Versi\u00f3n de API de Glances (2 o 3)" - } + }, + "title": "Configurar Glances" } } }, diff --git a/homeassistant/components/glances/translations/fi.json b/homeassistant/components/glances/translations/fi.json index 43ccf405d14..70a013677c0 100644 --- a/homeassistant/components/glances/translations/fi.json +++ b/homeassistant/components/glances/translations/fi.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Yhteyden muodostaminen palvelimeen ei onnistu" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/glances/translations/ko.json b/homeassistant/components/glances/translations/ko.json index d0e318a0454..2cf0aa1d595 100644 --- a/homeassistant/components/glances/translations/ko.json +++ b/homeassistant/components/glances/translations/ko.json @@ -19,7 +19,7 @@ "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778", "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)" }, - "title": "Glances \uc124\uce58" + "title": "Glances \uc124\uce58\ud558\uae30" } } }, diff --git a/homeassistant/components/glances/translations/no.json b/homeassistant/components/glances/translations/no.json index dd593c4add6..1a03d8f1e4c 100644 --- a/homeassistant/components/glances/translations/no.json +++ b/homeassistant/components/glances/translations/no.json @@ -13,7 +13,7 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "", + "port": "Port", "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet", "username": "Brukernavn", "verify_ssl": "Bekreft sertifiseringen av systemet", diff --git a/homeassistant/components/glances/translations/pl.json b/homeassistant/components/glances/translations/pl.json index 25179e951ad..2f36613b360 100644 --- a/homeassistant/components/glances/translations/pl.json +++ b/homeassistant/components/glances/translations/pl.json @@ -10,12 +10,12 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa", - "password": "Has\u0142o", - "port": "Port", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", - "username": "Nazwa u\u017cytkownika", + "username": "[%key_id:common::config_flow::data::username%]", "verify_ssl": "Sprawd\u017a certyfikacj\u0119 systemu", "version": "Glances wersja API (2 lub 3)" }, diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 62aea62bf84..68babd3debe 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -4,7 +4,7 @@ import logging from pygogogate2 import Gogogate2API as pygogogate2 import voluptuous as vol -from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverDevice +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, @@ -57,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class MyGogogate2Device(CoverDevice): +class MyGogogate2Device(CoverEntity): """Representation of a Gogogate2 cover.""" def __init__(self, mygogogate2, device, name): diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 2d848101def..160ec024c81 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -55,11 +55,8 @@ GOOGLE_SERVICE_ACCOUNT = vol.Schema( def _check_report_state(data): - if data[CONF_REPORT_STATE]: - if CONF_SERVICE_ACCOUNT not in data: - raise vol.Invalid( - "If report state is enabled, a service account must exist" - ) + if data[CONF_REPORT_STATE] and CONF_SERVICE_ACCOUNT not in data: + raise vol.Invalid("If report state is enabled, a service account must exist") return data diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 37fef6f2d79..9a133b5e6b7 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -124,6 +124,7 @@ DOMAIN_TO_GOOGLE_TYPES = { DEVICE_CLASS_TO_GOOGLE_TYPES = { (cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE, + (cover.DOMAIN, cover.DEVICE_CLASS_GATE): TYPE_GARAGE, (cover.DOMAIN, cover.DEVICE_CLASS_DOOR): TYPE_DOOR, (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6c2fa3d82e1..dd948f1fc51 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store from . import trait @@ -425,7 +426,7 @@ class GoogleEntity: "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.hass.http.server_port, "httpSSL": self.hass.config.api.use_ssl, - "baseUrl": self.hass.config.api.base_url, + "baseUrl": get_url(self.hass, prefer_external=True), "proxyDeviceId": agent_user_id, } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index ab045896235..3da91b2b612 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -50,6 +50,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.helpers.network import get_url from homeassistant.util import color as color_util, temperature as temp_util from .const import ( @@ -247,9 +248,7 @@ class CameraStreamTrait(_Trait): url = await self.hass.components.camera.async_request_stream( self.state.entity_id, "hls" ) - self.stream_info = { - "cameraStreamAccessUrl": self.hass.config.api.base_url + url - } + self.stream_info = {"cameraStreamAccessUrl": f"{get_url(self.hass)}{url}"} @register_trait diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index e7b18aacc15..2fa227f0953 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -7,7 +7,7 @@ import time import voluptuous as vol from websocket import _exceptions, create_connection -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -167,7 +167,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): setup_gpmdp(hass, config, code, add_entities) -class GPMDP(MediaPlayerDevice): +class GPMDP(MediaPlayerEntity): """Representation of a GPMDP.""" def __init__(self, name, url, code): diff --git a/homeassistant/components/gpslogger/translations/ko.json b/homeassistant/components/gpslogger/translations/ko.json index e0848849758..3c010cb65b9 100644 --- a/homeassistant/components/gpslogger/translations/ko.json +++ b/homeassistant/components/gpslogger/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "GPSLogger Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "GPSLogger Webhook \uc124\uc815" + "description": "GPSLogger \uc6f9 \ud6c5\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "GPSLogger \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 06a60ab3fd7..42f7c0334b3 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -108,7 +108,7 @@ class GEMSensor(Entity): @property def unique_id(self): """Return a unique ID for this sensor.""" - return f"{self._monitor_serial_number}-{self._sensor_type }-{self._number}" + return f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}" @property def name(self): diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 8b85de598b0..41e4b99b6c6 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class GreenwaveLight(Light): +class GreenwaveLight(LightEntity): """Representation of an Greenwave Reality Light.""" def __init__(self, light, host, token, gatewaydata): diff --git a/homeassistant/components/griddy/translations/es-419.json b/homeassistant/components/griddy/translations/es-419.json new file mode 100644 index 00000000000..652c8484b4e --- /dev/null +++ b/homeassistant/components/griddy/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Esta zona de carga ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "loadzone": "Zona de carga (punto de asentamiento)" + }, + "description": "Su zona de carga est\u00e1 en su cuenta de Griddy en \"Cuenta > Medidor > Zona de carga\".", + "title": "Configura tu zona de carga Griddy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/fr.json b/homeassistant/components/griddy/translations/fr.json index 845c0968827..c2fd4d8d627 100644 --- a/homeassistant/components/griddy/translations/fr.json +++ b/homeassistant/components/griddy/translations/fr.json @@ -11,7 +11,9 @@ "user": { "data": { "loadzone": "Zone de charge (point d'\u00e9tablissement)" - } + }, + "description": "Votre zone de charge se trouve dans votre compte Griddy sous \"Compte > Compteur > Zone de charge\".", + "title": "Configurez votre zone de charge Griddy" } } } diff --git a/homeassistant/components/griddy/translations/ko.json b/homeassistant/components/griddy/translations/ko.json index 9eda02551e4..a17db380aa0 100644 --- a/homeassistant/components/griddy/translations/ko.json +++ b/homeassistant/components/griddy/translations/ko.json @@ -13,7 +13,7 @@ "loadzone": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed (\uc815\uc0b0\uc810)" }, "description": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 Griddy \uacc4\uc815\uc758 \"Account > Meter > Load Zone\"\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "Griddy \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed \uc124\uc815" + "title": "Griddy \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/griddy/translations/nl.json b/homeassistant/components/griddy/translations/nl.json new file mode 100644 index 00000000000..9227d4702ab --- /dev/null +++ b/homeassistant/components/griddy/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Deze laadzone is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "description": "Uw Load Zone staat op uw Griddy account onder \"Account > Meter > Load Zone\".", + "title": "Stel uw Griddy Load Zone in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/pl.json b/homeassistant/components/griddy/translations/pl.json index 43a16be8d79..e62ce8f7bdc 100644 --- a/homeassistant/components/griddy/translations/pl.json +++ b/homeassistant/components/griddy/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { diff --git a/homeassistant/components/griddy/translations/sv.json b/homeassistant/components/griddy/translations/sv.json new file mode 100644 index 00000000000..e9ddacf2714 --- /dev/null +++ b/homeassistant/components/griddy/translations/sv.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index d9efdfa53c6..c4e691eeff9 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -27,7 +27,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, - CoverDevice, + CoverEntity, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -66,7 +66,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) -class CoverGroup(CoverDevice): +class CoverGroup(CoverEntity): """Representation of a CoverGroup.""" def __init__(self, name, entities): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 3abca98bd2c..56408f410b8 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -76,7 +76,7 @@ async def async_setup_platform( ) -class LightGroup(light.Light): +class LightGroup(light.LightEntity): """Representation of a light group.""" def __init__(self, name: str, entity_ids: List[str]) -> None: @@ -326,7 +326,7 @@ def _mean_int(*args): def _mean_tuple(*args): """Return the mean values along the columns of the supplied values.""" - return tuple(sum(l) / len(l) for l in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args)) def _reduce_attribute( diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index bbc9753909b..67e13aa7cfa 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -2,7 +2,7 @@ "state": { "_": { "closed": "Chiuso", - "home": "A casa", + "home": "In casa", "locked": "Bloccato", "not_home": "Fuori casa", "off": "Spento", diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index b41c930ac9f..763021190c1 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -1,3 +1,17 @@ { + "state": { + "_": { + "closed": "Lukket", + "home": "Hjemme", + "locked": "L\u00e5st", + "not_home": "Borte", + "off": "Av", + "ok": "", + "on": "P\u00e5", + "open": "\u00c5pen", + "problem": "", + "unlocked": "Ul\u00e5st" + } + }, "title": "Gruppe" } \ No newline at end of file diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 9b371bfffca..ea211ccd748 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -4,7 +4,7 @@ import logging from gsp import GstreamerPlayer import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -50,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([GstreamerDevice(player, name)]) -class GstreamerDevice(MediaPlayerDevice): +class GstreamerDevice(MediaPlayerEntity): """Representation of a Gstreamer device.""" def __init__(self, player, name): diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json index 8d5229c9941..0133bf421ff 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -12,16 +12,18 @@ "step": { "user": { "data": { - "email": "E-Mail Address", - "password": "Password", + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", "authorization_code": "Authorization Code (required for manual authentication)" }, "title": "Google Hangouts Login" }, "2fa": { - "data": { "2fa": "2FA Pin" }, + "data": { + "2fa": "2FA Pin" + }, "title": "2-Factor-Authentication" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json index bd2170a0f92..0c2c91d1ac8 100644 --- a/homeassistant/components/hangouts/translations/en.json +++ b/homeassistant/components/hangouts/translations/en.json @@ -19,7 +19,7 @@ "user": { "data": { "authorization_code": "Authorization Code (required for manual authentication)", - "email": "E-Mail Address", + "email": "Email", "password": "Password" }, "title": "Google Hangouts Login" diff --git a/homeassistant/components/hangouts/translations/es-419.json b/homeassistant/components/hangouts/translations/es-419.json index 9ff97592d91..a8ae41ec21e 100644 --- a/homeassistant/components/hangouts/translations/es-419.json +++ b/homeassistant/components/hangouts/translations/es-419.json @@ -6,6 +6,7 @@ }, "error": { "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, intente nuevamente.", + "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { diff --git a/homeassistant/components/hangouts/translations/fi.json b/homeassistant/components/hangouts/translations/fi.json new file mode 100644 index 00000000000..68bc9eb0389 --- /dev/null +++ b/homeassistant/components/hangouts/translations/fi.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "unknown": "Tapahtui tuntematon virhe." + }, + "step": { + "2fa": { + "title": "Kaksivaiheinen tunnistus" + }, + "user": { + "data": { + "email": "S\u00e4hk\u00f6postiosoite", + "password": "Salasana" + }, + "title": "Google Hangouts -kirjautuminen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ko.json b/homeassistant/components/hangouts/translations/ko.json index 4a5af0779a2..51bd857e358 100644 --- a/homeassistant/components/hangouts/translations/ko.json +++ b/homeassistant/components/hangouts/translations/ko.json @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "\uc778\uc99d \ucf54\ub4dc (\uc218\ub3d9 \uc778\uc99d\uc5d0 \ud544\uc694)", - "email": "\uc774\uba54\uc77c \uc8fc\uc18c", + "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json index 9402687b8ff..8818898c0b4 100644 --- a/homeassistant/components/hangouts/translations/no.json +++ b/homeassistant/components/hangouts/translations/no.json @@ -5,7 +5,7 @@ "unknown": "Ukjent feil oppstod." }, "error": { - "invalid_2fa": "Ugyldig tofaktorautentisering, vennligst pr\u00f8v igjen.", + "invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.", "invalid_2fa_method": "Ugyldig 2FA-metode (Bekreft p\u00e5 telefon).", "invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen." }, @@ -15,11 +15,11 @@ "2fa": "2FA Pin" }, "description": "Tom", - "title": "Tofaktorautentisering" + "title": "Totrinnsbekreftelse" }, "user": { "data": { - "authorization_code": "Autorisasjonskode (kreves for manuell godkjenning)", + "authorization_code": "Godkjenningskode (kreves for manuell godkjenning)", "email": "E-postadresse", "password": "Passord" }, diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json index 20b17ec37b7..f4a1d0a0fdd 100644 --- a/homeassistant/components/hangouts/translations/pl.json +++ b/homeassistant/components/hangouts/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts jest ju\u017c skonfigurowany.", - "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "invalid_2fa": "Nieprawid\u0142owe uwierzytelnienie dwusk\u0142adnikowe, spr\u00f3buj ponownie.", @@ -20,8 +20,8 @@ "user": { "data": { "authorization_code": "Kod autoryzacji (wymagany do r\u0119cznego uwierzytelnienia)", - "email": "Adres e-mail", - "password": "Has\u0142o" + "email": "[%key_id:common::config_flow::data::email%]", + "password": "[%key_id:common::config_flow::data::password%]" }, "description": "Pusty", "title": "Logowanie do Google Hangouts" diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index fd7cddcaed9..30a857f8b99 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -4,7 +4,7 @@ import logging import hkavr import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discover_info=None): add_entities([avr_device], True) -class HkAvrDevice(MediaPlayerDevice): +class HkAvrDevice(MediaPlayerEntity): """Representation of a Harman Kardon AVR / JBL AVR TV.""" def __init__(self, avr): diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 147ee75a863..25b68b42e72 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -125,7 +125,7 @@ async def async_setup_entry( ) -class HarmonyRemote(remote.RemoteDevice): +class HarmonyRemote(remote.RemoteEntity): """Remote representation used to control a Harmony device.""" def __init__(self, name, unique_id, host, activity, out_path, delay_secs): diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index e093d02051d..86de34672be 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -4,7 +4,10 @@ "step": { "user": { "title": "Setup Logitech Harmony Hub", - "data": { "host": "Hostname or IP Address", "name": "Hub Name" } + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "Hub Name" + } }, "link": { "title": "Setup Logitech Harmony Hub", @@ -15,7 +18,9 @@ "cannot_connect": "Failed to connect, please try again", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } }, "options": { "step": { @@ -28,4 +33,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json index a7c520c400c..90d8f064301 100644 --- a/homeassistant/components/harmony/translations/ca.json +++ b/homeassistant/components/harmony/translations/ca.json @@ -26,8 +26,8 @@ "step": { "init": { "data": { - "activity": "Activitat predeterminada a executar quan no se n\u2019especifica cap.", - "delay_secs": "Retard entre l\u2019enviament d\u2019ordres." + "activity": "Activitat predeterminada a executar quan no se n'especifica cap.", + "delay_secs": "Retard entre l'enviament d'ordres." }, "description": "Ajusta les opcions de Harmony Hub" } diff --git a/homeassistant/components/harmony/translations/en.json b/homeassistant/components/harmony/translations/en.json index 17964db6824..ce13e79e279 100644 --- a/homeassistant/components/harmony/translations/en.json +++ b/homeassistant/components/harmony/translations/en.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Hostname or IP Address", + "host": "Host", "name": "Hub Name" }, "title": "Setup Logitech Harmony Hub" diff --git a/homeassistant/components/harmony/translations/es-419.json b/homeassistant/components/harmony/translations/es-419.json new file mode 100644 index 00000000000..83781be522e --- /dev/null +++ b/homeassistant/components/harmony/translations/es-419.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "\u00bfDesea configurar {name} ({host})?", + "title": "Configurar Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nombre de host o direcci\u00f3n IP", + "name": "Nombre del concentrador" + }, + "title": "Configurar Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "La actividad predeterminada para ejecutar cuando no se especifica ninguno.", + "delay_secs": "El retraso entre el env\u00edo de comandos." + }, + "description": "Ajuste las opciones de Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 3272c8941c7..5751c22bbbe 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -11,14 +11,14 @@ "step": { "link": { "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Logitech Harmony Hub \uc124\uc815" + "title": "Logitech Harmony Hub \uc124\uc815\ud558\uae30" }, "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "host": "\ud638\uc2a4\ud2b8", "name": "Hub \uc774\ub984" }, - "title": "Logitech Harmony Hub \uc124\uc815" + "title": "Logitech Harmony Hub \uc124\uc815\ud558\uae30" } } }, diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json index a896cab0877..63d8026d9c2 100644 --- a/homeassistant/components/harmony/translations/nl.json +++ b/homeassistant/components/harmony/translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" }, "flow_title": "Logitech Harmony Hub {name}", "step": { @@ -20,5 +21,16 @@ "title": "Logitech Harmony Hub instellen" } } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "De standaardactiviteit die moet worden uitgevoerd wanneer er geen is opgegeven.", + "delay_secs": "De vertraging tussen het verzenden van opdrachten." + }, + "description": "Pas de Harmony Hub-opties aan" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/no.json b/homeassistant/components/harmony/translations/no.json index 9cae0663208..83e7a2f6cb7 100644 --- a/homeassistant/components/harmony/translations/no.json +++ b/homeassistant/components/harmony/translations/no.json @@ -11,14 +11,14 @@ "step": { "link": { "description": "Vil du konfigurere {name} ({host})?", - "title": "Oppsett Logitech Harmony Hub" + "title": "Sett opp Logitech Harmony Hub" }, "user": { "data": { "host": "Vertsnavn eller IP-adresse", "name": "Navn p\u00e5 hub" }, - "title": "Oppsett Logitech Harmony Hub" + "title": "Sett opp Logitech Harmony Hub" } } }, diff --git a/homeassistant/components/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json index 533c14097a5..a1d7ecfeca8 100644 --- a/homeassistant/components/harmony/translations/pl.json +++ b/homeassistant/components/harmony/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "flow_title": "Logitech Harmony Hub {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa huba" }, "title": "Konfiguracja Logitech Harmony Hub" diff --git a/homeassistant/components/harmony/translations/ru.json b/homeassistant/components/harmony/translations/ru.json index 85b61f923e1..4e995a26c48 100644 --- a/homeassistant/components/harmony/translations/ru.json +++ b/homeassistant/components/harmony/translations/ru.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "title": "Logitech Harmony Hub" diff --git a/homeassistant/components/harmony/translations/sv.json b/homeassistant/components/harmony/translations/sv.json new file mode 100644 index 00000000000..6e9c861763b --- /dev/null +++ b/homeassistant/components/harmony/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "link": { + "description": "Do vill du konfigurera {name} ({host})?" + }, + "user": { + "data": { + "host": "V\u00e4rdnamn eller IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index 5b4171de5cc..dfd1249d629 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "name": "Hub \u540d\u7a31" }, "title": "\u8a2d\u5b9a\u7f85\u6280 Harmony Hub" diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index f13db03ca4c..6a383e28ff1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -41,7 +41,8 @@ CONFIG_SCHEMA = vol.Schema( ) -DATA_HOMEASSISTANT_VERSION = "hassio_hass_version" +DATA_INFO = "hassio_info" +DATA_HOST_INFO = "hassio_host_info" HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) SERVICE_ADDON_START = "addon_start" @@ -130,7 +131,29 @@ def get_homeassistant_version(hass): Async friendly. """ - return hass.data.get(DATA_HOMEASSISTANT_VERSION) + if DATA_INFO not in hass.data: + return None + return hass.data[DATA_INFO].get("homeassistant") + + +@callback +@bind_hass +def get_info(hass): + """Return generic information from Supervisor. + + Async friendly. + """ + return hass.data.get(DATA_INFO) + + +@callback +@bind_hass +def get_host_info(hass): + """Return generic host information. + + Async friendly. + """ + return hass.data.get(DATA_HOST_INFO) @callback @@ -247,20 +270,20 @@ async def async_setup(hass, config): DOMAIN, service, async_service_handler, schema=settings[1] ) - async def update_homeassistant_version(now): - """Update last available Home Assistant version.""" + async def update_info_data(now): + """Update last available supervisor information.""" try: - data = await hassio.get_homeassistant_info() - hass.data[DATA_HOMEASSISTANT_VERSION] = data["last_version"] + hass.data[DATA_INFO] = await hassio.get_info() + hass.data[DATA_HOST_INFO] = await hassio.get_host_info() except HassioAPIError as err: _LOGGER.warning("Can't read last version: %s", err) hass.helpers.event.async_track_point_in_utc_time( - update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL + update_info_data, utcnow() + HASSIO_UPDATE_INTERVAL ) # Fetch last version - await update_homeassistant_version(None) + await update_info_data(None) async def async_handle_core_service(call): """Service handler for handling core services.""" diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index d8616a82578..9e44b961a1c 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -75,12 +75,9 @@ class HassIOAddonPanel(HomeAssistantView): return {} -def _register_panel(hass, addon, data): - """Init coroutine to register the panel. - - Return coroutine. - """ - return hass.components.panel_custom.async_register_panel( +async def _register_panel(hass, addon, data): + """Init coroutine to register the panel.""" + await hass.components.panel_custom.async_register_panel( frontend_url_path=addon, webcomponent_name="hassio-main", sidebar_title=data[ATTR_TITLE], diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index d929f2d3e82..9d3df7e8aec 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -68,12 +68,20 @@ class HassIO: return self.send_command("/supervisor/ping", method="get", timeout=15) @_api_data - def get_homeassistant_info(self): - """Return data for Home Assistant. + def get_info(self): + """Return generic Supervisor information. This method return a coroutine. """ - return self.send_command("/homeassistant/info", method="get") + return self.send_command("/info", method="get") + + @_api_data + def get_host_info(self): + """Return data for Host. + + This method return a coroutine. + """ + return self.send_command("/host/info", method="get") @_api_data def get_addon_info(self, addon): diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 981cb51c83a..d8a4c453015 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index c49ee45271a..180580ef371 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -22,7 +22,7 @@ from pycec.const import ( TYPE_TUNER, ) -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_NEXT_TRACK, @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class CecPlayerDevice(CecDevice, MediaPlayerDevice): +class CecPlayerDevice(CecDevice, MediaPlayerEntity): """Representation of a HDMI device as a Media player.""" def __init__(self, device, logical) -> None: diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index 0fcf9c01c8f..aaaa2b83054 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -1,7 +1,7 @@ """Support for HDMI CEC devices as switches.""" import logging -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY from . import ATTR_NEW, CecDevice @@ -22,7 +22,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class CecSwitchDevice(CecDevice, SwitchDevice): +class CecSwitchDevice(CecDevice, SwitchEntity): """Representation of a HDMI device as a Switch.""" def __init__(self, device, logical) -> None: diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 553ae8f4bc3..b3f3363818c 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -9,7 +9,7 @@ from homeassistant.components.climate import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, PLATFORM_SCHEMA, - ClimateDevice, + ClimateEntity, ) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( @@ -64,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class HeatmiserV3Thermostat(ClimateDevice): +class HeatmiserV3Thermostat(ClimateEntity): """Representation of a HeatmiserV3 thermostat.""" def __init__(self, therm, device, uh1): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 39c5a9928af..7e827c96f55 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -6,7 +6,7 @@ from typing import Sequence from pyheos import HeosError, const as heos_const -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, @@ -85,7 +85,7 @@ def log_command_error(command: str): return decorator -class HeosMediaPlayer(MediaPlayerDevice): +class HeosMediaPlayer(MediaPlayerEntity): """The HEOS player.""" def __init__(self, player): diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 383afad1b96..b633b3c82b6 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -4,7 +4,9 @@ "user": { "title": "Connect to Heos", "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", - "data": { "access_token": "Host", "host": "Host" } + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } }, "error": { diff --git a/homeassistant/components/heos/translations/es-419.json b/homeassistant/components/heos/translations/es-419.json index 01338dc5af3..902f65bf5e1 100644 --- a/homeassistant/components/heos/translations/es-419.json +++ b/homeassistant/components/heos/translations/es-419.json @@ -8,6 +8,10 @@ }, "step": { "user": { + "data": { + "access_token": "Host", + "host": "Host" + }, "description": "Ingrese el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", "title": "Con\u00e9ctate a Heos" } diff --git a/homeassistant/components/heos/translations/fi.json b/homeassistant/components/heos/translations/fi.json new file mode 100644 index 00000000000..73e50dbf8ce --- /dev/null +++ b/homeassistant/components/heos/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "Palvelin" + }, + "title": "Yhdist\u00e4 Heosiin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/it.json b/homeassistant/components/heos/translations/it.json index 2e6cfab035b..c40bbb95554 100644 --- a/homeassistant/components/heos/translations/it.json +++ b/homeassistant/components/heos/translations/it.json @@ -13,7 +13,7 @@ "host": "Host" }, "description": "Inserire il nome host o l'indirizzo IP di un dispositivo Heos (preferibilmente uno connesso alla rete tramite cavo).", - "title": "Connetti a Heos" + "title": "Collegamento a Heos" } } } diff --git a/homeassistant/components/heos/translations/no.json b/homeassistant/components/heos/translations/no.json index 25588d79e01..706fbc31cb4 100644 --- a/homeassistant/components/heos/translations/no.json +++ b/homeassistant/components/heos/translations/no.json @@ -12,7 +12,7 @@ "access_token": "Vert", "host": "Vert" }, - "description": "Vennligst skriv inn vertsnavnet eller IP-adressen til en Heos-enhet (helst en tilkoblet via kabel til nettverket).", + "description": "Vennligst fyll inn vertsnavnet eller IP-adressen til en Heos-enhet (helst en tilkoblet nettverket via kabel).", "title": "Koble til Heos" } } diff --git a/homeassistant/components/heos/translations/pl.json b/homeassistant/components/heos/translations/pl.json index 0c0b9ade13f..a421e92dd8a 100644 --- a/homeassistant/components/heos/translations/pl.json +++ b/homeassistant/components/heos/translations/pl.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "access_token": "Nazwa hosta lub adres IP", - "host": "Nazwa hosta lub adres IP" + "access_token": "[%key_id:common::config_flow::data::host%]", + "host": "[%key_id:common::config_flow::data::host%]" }, "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (najlepiej pod\u0142\u0105czonego przewodowo do sieci).", "title": "Po\u0142\u0105czenie z Heos" diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 4d2a879bc73..779afa10cca 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -5,7 +5,7 @@ import logging from pyhik.hikvision import HikCamera import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE, @@ -183,7 +183,7 @@ class HikvisionData: return self.camdata.fetch_attributes(sensor, channel) -class HikvisionBinarySensor(BinarySensorDevice): +class HikvisionBinarySensor(BinarySensorEntity): """Representation of a Hikvision binary sensor.""" def __init__(self, hass, sensor, channel, cam, delay): diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index f86853a5468..2e924135bd4 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -5,7 +5,7 @@ import hikvision.api from hikvision.error import HikvisionError, MissingParamError 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_HOST, CONF_NAME, @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([HikvisionMotionSwitch(name, hikvision_cam)]) -class HikvisionMotionSwitch(SwitchDevice): +class HikvisionMotionSwitch(SwitchEntity): """Representation of a switch to toggle on/off motion detection.""" def __init__(self, name, hikvision_cam): diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index da18419c264..23a3a0c1416 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -5,7 +5,7 @@ import logging from pyaehw4a1.aehw4a1 import AehW4a1 import pyaehw4a1.exceptions -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, FAN_HIGH, @@ -144,7 +144,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class ClimateAehW4a1(ClimateDevice): +class ClimateAehW4a1(ClimateEntity): """Representation of a Hisense AEH-W4A1 module for climate device.""" def __init__(self, device): diff --git a/homeassistant/components/hisense_aehw4a1/translations/es-419.json b/homeassistant/components/hisense_aehw4a1/translations/es-419.json new file mode 100644 index 00000000000..c9c4270360a --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Hisense AEH-W4A1 en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/no.json b/homeassistant/components/hisense_aehw4a1/translations/no.json index 65c9968dc1e..bc048ef2286 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/no.json +++ b/homeassistant/components/hisense_aehw4a1/translations/no.json @@ -6,8 +6,8 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Vil du sette opp Hisense AEH-W4A1?", + "title": "" } } } diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index fa91d6862a2..27c648f554b 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,5 +1,5 @@ """Support for the Hive binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DATA_HIVE, DOMAIN, HiveEntity @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class HiveBinarySensorEntity(HiveEntity, BinarySensorDevice): +class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): """Representation of a Hive binary sensor.""" @property diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 202cea7bf8e..33c8fed4eca 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,5 +1,5 @@ """Support for the Hive climate devices.""" -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class HiveClimateEntity(HiveEntity, ClimateDevice): +class HiveClimateEntity(HiveEntity, ClimateEntity): """Hive Climate Device.""" def __init__(self, hive_session, hive_device): diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 33175de543d..d6a9d1f400b 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -6,7 +6,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) import homeassistant.util.color as color_util @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class HiveDeviceLight(HiveEntity, Light): +class HiveDeviceLight(HiveEntity, LightEntity): """Hive Active Light Device.""" def __init__(self, hive_session, hive_device): diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 53e1ec6a069..734581b0db3 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,5 +1,5 @@ """Support for the Hive switches.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class HiveDevicePlug(HiveEntity, SwitchDevice): +class HiveDevicePlug(HiveEntity, SwitchEntity): """Hive Active Plug.""" @property diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index d7d98426df5..693fd6f322b 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -4,7 +4,7 @@ from homeassistant.components.water_heater import ( STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import TEMP_CELSIUS @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class HiveWaterHeater(HiveEntity, WaterHeaterDevice): +class HiveWaterHeater(HiveEntity, WaterHeaterEntity): """Hive Water Heater Device.""" @property diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py new file mode 100644 index 00000000000..4e575963577 --- /dev/null +++ b/homeassistant/components/home_connect/__init__.py @@ -0,0 +1,106 @@ +"""Support for BSH Home Connect appliances.""" + +import asyncio +from datetime import timedelta +import logging + +from requests import HTTPError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.util import Throttle + +from . import api, config_flow +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +PLATFORMS = ["binary_sensor", "sensor", "switch"] + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up Home Connect component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Home Connect from a config entry.""" + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + hc_api = api.ConfigEntryAuth(hass, entry, implementation) + + hass.data[DOMAIN][entry.entry_id] = hc_api + + await update_all_devices(hass, entry) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +@Throttle(SCAN_INTERVAL) +async def update_all_devices(hass, entry): + """Update all the devices.""" + data = hass.data[DOMAIN] + hc_api = data[entry.entry_id] + try: + await hass.async_add_executor_job(hc_api.get_devices) + for device_dict in hc_api.devices: + await hass.async_add_executor_job(device_dict["device"].initialize) + except HTTPError as err: + _LOGGER.warning("Cannot update devices: %s", err.response.status_code) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py new file mode 100644 index 00000000000..a208f9c7f0f --- /dev/null +++ b/homeassistant/components/home_connect/api.py @@ -0,0 +1,372 @@ +"""API for Home Connect bound to HASS OAuth.""" + +from asyncio import run_coroutine_threadsafe +import logging + +import homeconnect +from homeconnect.api import HomeConnectError + +from homeassistant import config_entries, core +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, TIME_SECONDS, UNIT_PERCENTAGE +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import ( + BSH_ACTIVE_PROGRAM, + BSH_POWER_OFF, + BSH_POWER_STANDBY, + SIGNAL_UPDATE_ENTITIES, +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryAuth(homeconnect.HomeConnectAPI): + """Provide Home Connect authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Home Connect Auth.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token) + self.devices = [] + + def refresh_tokens(self) -> dict: + """Refresh and return new Home Connect tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token + + def get_devices(self): + """Get a dictionary of devices.""" + appl = self.get_appliances() + devices = [] + for app in appl: + if app.type == "Dryer": + device = Dryer(self.hass, app) + elif app.type == "Washer": + device = Washer(self.hass, app) + elif app.type == "Dishwasher": + device = Dishwasher(self.hass, app) + elif app.type == "FridgeFreezer": + device = FridgeFreezer(self.hass, app) + elif app.type == "Oven": + device = Oven(self.hass, app) + elif app.type == "CoffeeMaker": + device = CoffeeMaker(self.hass, app) + elif app.type == "Hood": + device = Hood(self.hass, app) + elif app.type == "Hob": + device = Hob(self.hass, app) + else: + _LOGGER.warning("Appliance type %s not implemented.", app.type) + continue + devices.append({"device": device, "entities": device.get_entity_info()}) + self.devices = devices + return devices + + +class HomeConnectDevice: + """Generic Home Connect device.""" + + # for some devices, this is instead BSH_POWER_STANDBY + # see https://developer.home-connect.com/docs/settings/power_state + power_off_state = BSH_POWER_OFF + + def __init__(self, hass, appliance): + """Initialize the device class.""" + self.hass = hass + self.appliance = appliance + + def initialize(self): + """Fetch the info needed to initialize the device.""" + try: + self.appliance.get_status() + except (HomeConnectError, ValueError): + _LOGGER.debug("Unable to fetch appliance status. Probably offline.") + try: + self.appliance.get_settings() + except (HomeConnectError, ValueError): + _LOGGER.debug("Unable to fetch settings. Probably offline.") + try: + program_active = self.appliance.get_programs_active() + except (HomeConnectError, ValueError): + _LOGGER.debug("Unable to fetch active programs. Probably offline.") + program_active = None + if program_active and "key" in program_active: + self.appliance.status[BSH_ACTIVE_PROGRAM] = {"value": program_active["key"]} + self.appliance.listen_events(callback=self.event_callback) + + def event_callback(self, appliance): + """Handle event.""" + _LOGGER.debug("Update triggered on %s", appliance.name) + _LOGGER.debug(self.appliance.status) + dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId) + + +class DeviceWithPrograms(HomeConnectDevice): + """Device with programs.""" + + PROGRAMS = [] + + def get_programs_available(self): + """Get the available programs.""" + return self.PROGRAMS + + def get_program_switches(self): + """Get a dictionary with info about program switches. + + There will be one switch for each program. + """ + programs = self.get_programs_available() + return [{"device": self, "program_name": p["name"]} for p in programs] + + def get_program_sensors(self): + """Get a dictionary with info about program sensors. + + There will be one of the four types of sensors for each + device. + """ + sensors = { + "Remaining Program Time": (None, None, DEVICE_CLASS_TIMESTAMP, 1), + "Duration": (TIME_SECONDS, "mdi:update", None, 1), + "Program Progress": (UNIT_PERCENTAGE, "mdi:progress-clock", None, 1), + } + return [ + { + "device": self, + "desc": k, + "unit": unit, + "key": "BSH.Common.Option.{}".format(k.replace(" ", "")), + "icon": icon, + "device_class": device_class, + "sign": sign, + } + for k, (unit, icon, device_class, sign) in sensors.items() + ] + + +class DeviceWithDoor(HomeConnectDevice): + """Device that has a door sensor.""" + + def get_door_entity(self): + """Get a dictionary with info about the door binary sensor.""" + return { + "device": self, + "desc": "Door", + "device_class": "door", + } + + +class Dryer(DeviceWithDoor, DeviceWithPrograms): + """Dryer class.""" + + PROGRAMS = [ + {"name": "LaundryCare.Dryer.Program.Cotton"}, + {"name": "LaundryCare.Dryer.Program.Synthetic"}, + {"name": "LaundryCare.Dryer.Program.Mix"}, + {"name": "LaundryCare.Dryer.Program.Blankets"}, + {"name": "LaundryCare.Dryer.Program.BusinessShirts"}, + {"name": "LaundryCare.Dryer.Program.DownFeathers"}, + {"name": "LaundryCare.Dryer.Program.Hygiene"}, + {"name": "LaundryCare.Dryer.Program.Jeans"}, + {"name": "LaundryCare.Dryer.Program.Outdoor"}, + {"name": "LaundryCare.Dryer.Program.SyntheticRefresh"}, + {"name": "LaundryCare.Dryer.Program.Towels"}, + {"name": "LaundryCare.Dryer.Program.Delicates"}, + {"name": "LaundryCare.Dryer.Program.Super40"}, + {"name": "LaundryCare.Dryer.Program.Shirts15"}, + {"name": "LaundryCare.Dryer.Program.Pillow"}, + {"name": "LaundryCare.Dryer.Program.AntiShrink"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class Dishwasher(DeviceWithDoor, DeviceWithPrograms): + """Dishwasher class.""" + + PROGRAMS = [ + {"name": "Dishcare.Dishwasher.Program.Auto1"}, + {"name": "Dishcare.Dishwasher.Program.Auto2"}, + {"name": "Dishcare.Dishwasher.Program.Auto3"}, + {"name": "Dishcare.Dishwasher.Program.Eco50"}, + {"name": "Dishcare.Dishwasher.Program.Quick45"}, + {"name": "Dishcare.Dishwasher.Program.Intensiv70"}, + {"name": "Dishcare.Dishwasher.Program.Normal65"}, + {"name": "Dishcare.Dishwasher.Program.Glas40"}, + {"name": "Dishcare.Dishwasher.Program.GlassCare"}, + {"name": "Dishcare.Dishwasher.Program.NightWash"}, + {"name": "Dishcare.Dishwasher.Program.Quick65"}, + {"name": "Dishcare.Dishwasher.Program.Normal45"}, + {"name": "Dishcare.Dishwasher.Program.Intensiv45"}, + {"name": "Dishcare.Dishwasher.Program.AutoHalfLoad"}, + {"name": "Dishcare.Dishwasher.Program.IntensivPower"}, + {"name": "Dishcare.Dishwasher.Program.MagicDaily"}, + {"name": "Dishcare.Dishwasher.Program.Super60"}, + {"name": "Dishcare.Dishwasher.Program.Kurz60"}, + {"name": "Dishcare.Dishwasher.Program.ExpressSparkle65"}, + {"name": "Dishcare.Dishwasher.Program.MachineCare"}, + {"name": "Dishcare.Dishwasher.Program.SteamFresh"}, + {"name": "Dishcare.Dishwasher.Program.MaximumCleaning"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class Oven(DeviceWithDoor, DeviceWithPrograms): + """Oven class.""" + + PROGRAMS = [ + {"name": "Cooking.Oven.Program.HeatingMode.PreHeating"}, + {"name": "Cooking.Oven.Program.HeatingMode.HotAir"}, + {"name": "Cooking.Oven.Program.HeatingMode.TopBottomHeating"}, + {"name": "Cooking.Oven.Program.HeatingMode.PizzaSetting"}, + {"name": "Cooking.Oven.Program.Microwave.600Watt"}, + ] + + power_off_state = BSH_POWER_STANDBY + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class Washer(DeviceWithDoor, DeviceWithPrograms): + """Washer class.""" + + PROGRAMS = [ + {"name": "LaundryCare.Washer.Program.Cotton"}, + {"name": "LaundryCare.Washer.Program.Cotton.CottonEco"}, + {"name": "LaundryCare.Washer.Program.EasyCare"}, + {"name": "LaundryCare.Washer.Program.Mix"}, + {"name": "LaundryCare.Washer.Program.DelicatesSilk"}, + {"name": "LaundryCare.Washer.Program.Wool"}, + {"name": "LaundryCare.Washer.Program.Sensitive"}, + {"name": "LaundryCare.Washer.Program.Auto30"}, + {"name": "LaundryCare.Washer.Program.Auto40"}, + {"name": "LaundryCare.Washer.Program.Auto60"}, + {"name": "LaundryCare.Washer.Program.Chiffon"}, + {"name": "LaundryCare.Washer.Program.Curtains"}, + {"name": "LaundryCare.Washer.Program.DarkWash"}, + {"name": "LaundryCare.Washer.Program.Dessous"}, + {"name": "LaundryCare.Washer.Program.Monsoon"}, + {"name": "LaundryCare.Washer.Program.Outdoor"}, + {"name": "LaundryCare.Washer.Program.PlushToy"}, + {"name": "LaundryCare.Washer.Program.ShirtsBlouses"}, + {"name": "LaundryCare.Washer.Program.SportFitness"}, + {"name": "LaundryCare.Washer.Program.Towels"}, + {"name": "LaundryCare.Washer.Program.WaterProof"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class CoffeeMaker(DeviceWithPrograms): + """Coffee maker class.""" + + PROGRAMS = [ + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Americano"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoDoppio"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.FlatWhite"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Galao"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.MilkFroth"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.WarmMilk"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Ristretto"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Cortado"}, + ] + + power_off_state = BSH_POWER_STANDBY + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return {"switch": program_switches, "sensor": program_sensors} + + +class Hood(DeviceWithPrograms): + """Hood class.""" + + PROGRAMS = [ + {"name": "Cooking.Common.Program.Hood.Automatic"}, + {"name": "Cooking.Common.Program.Hood.Venting"}, + {"name": "Cooking.Common.Program.Hood.DelayedShutOff"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return {"switch": program_switches, "sensor": program_sensors} + + +class FridgeFreezer(DeviceWithDoor): + """Fridge/Freezer class.""" + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + return {"binary_sensor": [door_entity]} + + +class Hob(DeviceWithPrograms): + """Hob class.""" + + PROGRAMS = [{"name": "Cooking.Hob.Program.PowerLevelMode"}] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return {"switch": program_switches, "sensor": program_sensors} diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py new file mode 100644 index 00000000000..4810231b432 --- /dev/null +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -0,0 +1,65 @@ +"""Provides a binary sensor for Home Connect.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity + +from .const import BSH_DOOR_STATE, DOMAIN +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect binary sensor.""" + + def get_entities(): + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("binary_sensor", []) + entities += [HomeConnectBinarySensor(**d) for d in entity_dicts] + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): + """Binary sensor for Home Connect.""" + + def __init__(self, device, desc, device_class): + """Initialize the entity.""" + super().__init__(device, desc) + self._device_class = device_class + self._state = None + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return bool(self._state) + + @property + def available(self): + """Return true if the binary sensor is available.""" + return self._state is not None + + async def async_update(self): + """Update the binary sensor's status.""" + state = self.device.appliance.status.get(BSH_DOOR_STATE, {}) + if not state: + self._state = None + elif state.get("value") in [ + "BSH.Common.EnumType.DoorState.Closed", + "BSH.Common.EnumType.DoorState.Locked", + ]: + self._state = False + elif state.get("value") == "BSH.Common.EnumType.DoorState.Open": + self._state = True + else: + _LOGGER.warning("Unexpected value for HomeConnect door state: %s", state) + self._state = None + _LOGGER.debug("Updated, new state: %s", self._state) + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/home_connect/config_flow.py b/homeassistant/components/home_connect/config_flow.py new file mode 100644 index 00000000000..4a714bac73f --- /dev/null +++ b/homeassistant/components/home_connect/config_flow.py @@ -0,0 +1,23 @@ +"""Config flow for Home Connect.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Home Connect OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py new file mode 100644 index 00000000000..10eb5dfd1e3 --- /dev/null +++ b/homeassistant/components/home_connect/const.py @@ -0,0 +1,16 @@ +"""Constants for the Home Connect integration.""" + +DOMAIN = "home_connect" + +OAUTH2_AUTHORIZE = "https://api.home-connect.com/security/oauth/authorize" +OAUTH2_TOKEN = "https://api.home-connect.com/security/oauth/token" + +BSH_POWER_STATE = "BSH.Common.Setting.PowerState" +BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On" +BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off" +BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby" +BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" +BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" +BSH_DOOR_STATE = "BSH.Common.Status.DoorState" + +SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py new file mode 100644 index 00000000000..12f86059023 --- /dev/null +++ b/homeassistant/components/home_connect/entity.py @@ -0,0 +1,67 @@ +"""Home Connect entity base class.""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .api import HomeConnectDevice +from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES + +_LOGGER = logging.getLogger(__name__) + + +class HomeConnectEntity(Entity): + """Generic Home Connect entity (base class).""" + + def __init__(self, device: HomeConnectDevice, desc: str) -> None: + """Initialize the entity.""" + self.device = device + self.desc = desc + self._name = f"{self.device.appliance.name} {desc}" + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ENTITIES, self._update_callback + ) + ) + + @callback + def _update_callback(self, ha_id): + """Update data.""" + if ha_id == self.device.appliance.haId: + self.async_entity_update() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the node (used for Entity_ID).""" + return self._name + + @property + def unique_id(self): + """Return the unique id base on the id returned by Home Connect and the entity name.""" + return f"{self.device.appliance.haId}-{self.desc}" + + @property + def device_info(self): + """Return info about the device.""" + return { + "identifiers": {(DOMAIN, self.device.appliance.haId)}, + "name": self.device.appliance.name, + "manufacturer": self.device.appliance.brand, + "model": self.device.appliance.vib, + } + + @callback + def async_entity_update(self): + """Update the entity.""" + _LOGGER.debug("Entity update triggered on %s", self) + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json new file mode 100644 index 00000000000..5c330f760b0 --- /dev/null +++ b/homeassistant/components/home_connect/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "home_connect", + "name": "Home Connect", + "documentation": "https://www.home-assistant.io/integrations/home_connect", + "dependencies": ["http"], + "codeowners": ["@DavidMStraub"], + "requirements": ["homeconnect==0.5"], + "config_flow": true +} diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py new file mode 100644 index 00000000000..0ae5a9fcd36 --- /dev/null +++ b/homeassistant/components/home_connect/sensor.py @@ -0,0 +1,92 @@ +"""Provides a sensor for Home Connect.""" + +from datetime import timedelta +import logging + +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect sensor.""" + + def get_entities(): + """Get a list of entities.""" + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("sensor", []) + entities += [HomeConnectSensor(**d) for d in entity_dicts] + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectSensor(HomeConnectEntity): + """Sensor class for Home Connect.""" + + def __init__(self, device, desc, key, unit, icon, device_class, sign=1): + """Initialize the entity.""" + super().__init__(device, desc) + self._state = None + self._key = key + self._unit = unit + self._icon = icon + self._device_class = device_class + self._sign = sign + + @property + def state(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def available(self): + """Return true if the sensor is available.""" + return self._state is not None + + async def async_update(self): + """Update the sensos status.""" + status = self.device.appliance.status + if self._key not in status: + self._state = None + else: + if self.device_class == DEVICE_CLASS_TIMESTAMP: + if "value" not in status[self._key]: + self._state = None + elif ( + self._state is not None + and self._sign == 1 + and dt_util.parse_datetime(self._state) < dt_util.utcnow() + ): + # if the date is supposed to be in the future but we're + # already past it, set state to None. + self._state = None + else: + seconds = self._sign * float(status[self._key]["value"]) + self._state = ( + dt_util.utcnow() + timedelta(seconds=seconds) + ).isoformat() + else: + self._state = status[self._key].get("value") + _LOGGER.debug("Updated, new state: %s", self._state) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json new file mode 100644 index 00000000000..6125897c962 --- /dev/null +++ b/homeassistant/components/home_connect/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "missing_configuration": "The Home Connect component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Home Connect." + } + } +} diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py new file mode 100644 index 00000000000..c5fcdef25b7 --- /dev/null +++ b/homeassistant/components/home_connect/switch.py @@ -0,0 +1,158 @@ +"""Provides a switch for Home Connect.""" +import logging + +from homeconnect.api import HomeConnectError + +from homeassistant.components.switch import SwitchEntity + +from .const import ( + BSH_ACTIVE_PROGRAM, + BSH_OPERATION_STATE, + BSH_POWER_ON, + BSH_POWER_STATE, + DOMAIN, +) +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect switch.""" + + def get_entities(): + """Get a list of entities.""" + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("switch", []) + entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts] + entity_list += [HomeConnectPowerSwitch(device_dict["device"])] + entities += entity_list + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): + """Switch class for Home Connect.""" + + def __init__(self, device, program_name): + """Initialize the entity.""" + desc = " ".join(["Program", program_name.split(".")[-1]]) + super().__init__(device, desc) + self.program_name = program_name + self._state = None + self._remote_allowed = None + + @property + def is_on(self): + """Return true if the switch is on.""" + return bool(self._state) + + @property + def available(self): + """Return true if the entity is available.""" + return True + + async def async_turn_on(self, **kwargs): + """Start the program.""" + _LOGGER.debug("Tried to turn on program %s", self.program_name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.start_program, self.program_name + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to start program: %s", err) + self.async_entity_update() + + async def async_turn_off(self, **kwargs): + """Stop the program.""" + _LOGGER.debug("Tried to stop program %s", self.program_name) + try: + await self.hass.async_add_executor_job(self.device.appliance.stop_program) + except HomeConnectError as err: + _LOGGER.error("Error while trying to stop program: %s", err) + self.async_entity_update() + + async def async_update(self): + """Update the switch's status.""" + state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {}) + if state.get("value") == self.program_name: + self._state = True + else: + self._state = False + _LOGGER.debug("Updated, new state: %s", self._state) + + +class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): + """Power switch class for Home Connect.""" + + def __init__(self, device): + """Inititialize the entity.""" + super().__init__(device, "Power") + self._state = None + + @property + def is_on(self): + """Return true if the switch is on.""" + return bool(self._state) + + async def async_turn_on(self, **kwargs): + """Switch the device on.""" + _LOGGER.debug("Tried to switch on %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn on device: %s", err) + self._state = False + self.async_entity_update() + + async def async_turn_off(self, **kwargs): + """Switch the device off.""" + _LOGGER.debug("tried to switch off %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + BSH_POWER_STATE, + self.device.power_off_state, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn off device: %s", err) + self._state = True + self.async_entity_update() + + async def async_update(self): + """Update the switch's status.""" + if ( + self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value") + == BSH_POWER_ON + ): + self._state = True + elif ( + self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value") + == self.device.power_off_state + ): + self._state = False + elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get( + "value", None + ) in [ + "BSH.Common.EnumType.OperationState.Ready", + "BSH.Common.EnumType.OperationState.DelayedStart", + "BSH.Common.EnumType.OperationState.Run", + "BSH.Common.EnumType.OperationState.Pause", + "BSH.Common.EnumType.OperationState.ActionRequired", + "BSH.Common.EnumType.OperationState.Aborting", + "BSH.Common.EnumType.OperationState.Finished", + ]: + self._state = True + elif ( + self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get("value") + == "BSH.Common.EnumType.OperationState.Inactive" + ): + self._state = False + else: + self._state = None + _LOGGER.debug("Updated, new state: %s", self._state) diff --git a/homeassistant/components/home_connect/translations/ca.json b/homeassistant/components/home_connect/translations/ca.json new file mode 100644 index 00000000000..be6054f9bda --- /dev/null +++ b/homeassistant/components/home_connect/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "El component Home Connect no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Home Connect." + }, + "step": { + "pick_implementation": { + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/de.json b/homeassistant/components/home_connect/translations/de.json new file mode 100644 index 00000000000..05204c35c41 --- /dev/null +++ b/homeassistant/components/home_connect/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Die Komponente Home Connect ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + }, + "create_entry": { + "default": "Erfolgreich mit Home Connect authentifiziert." + }, + "step": { + "pick_implementation": { + "title": "Authentifizierungsmethode ausw\u00e4hlen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/en.json b/homeassistant/components/home_connect/translations/en.json new file mode 100644 index 00000000000..78310536205 --- /dev/null +++ b/homeassistant/components/home_connect/translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "The Home Connect component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Home Connect." + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json new file mode 100644 index 00000000000..7457f7487d4 --- /dev/null +++ b/homeassistant/components/home_connect/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "El componente Home Connect no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Home Assistant." + }, + "step": { + "pick_implementation": { + "title": "Selecciona el M\u00e9todo de Autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/fr.json b/homeassistant/components/home_connect/translations/fr.json new file mode 100644 index 00000000000..630960b1c91 --- /dev/null +++ b/homeassistant/components/home_connect/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Le composant Home Connect n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentification r\u00e9ussie avec Home Connect." + }, + "step": { + "pick_implementation": { + "title": "Choisissez la m\u00e9thode d'authentification" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json new file mode 100644 index 00000000000..31804cfc421 --- /dev/null +++ b/homeassistant/components/home_connect/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/it.json b/homeassistant/components/home_connect/translations/it.json new file mode 100644 index 00000000000..3899d3b749f --- /dev/null +++ b/homeassistant/components/home_connect/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Il componente Home Connect non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticazione riuscita con Home Connect." + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json new file mode 100644 index 00000000000..973e1a0ec88 --- /dev/null +++ b/homeassistant/components/home_connect/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Home Connect \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "Home Connect \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/lb.json b/homeassistant/components/home_connect/translations/lb.json new file mode 100644 index 00000000000..1820e1e2788 --- /dev/null +++ b/homeassistant/components/home_connect/translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Home Connecz Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Home Connect authentifiz\u00e9iert." + }, + "step": { + "pick_implementation": { + "title": "Wiel Authentifikatiouns Method aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/no.json b/homeassistant/components/home_connect/translations/no.json new file mode 100644 index 00000000000..908f62efbc9 --- /dev/null +++ b/homeassistant/components/home_connect/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Home Connect-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + }, + "create_entry": { + "default": "Vellykket godkjenning med Home Connect" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/pl.json b/homeassistant/components/home_connect/translations/pl.json new file mode 100644 index 00000000000..08e4860453a --- /dev/null +++ b/homeassistant/components/home_connect/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Home Connect." + }, + "step": { + "pick_implementation": { + "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/ru.json b/homeassistant/components/home_connect/translations/ru.json new file mode 100644 index 00000000000..2ef6f7d6697 --- /dev/null +++ b/homeassistant/components/home_connect/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/sl.json b/homeassistant/components/home_connect/translations/sl.json new file mode 100644 index 00000000000..1e38c3851f2 --- /dev/null +++ b/homeassistant/components/home_connect/translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Komponenta Home Connect ni konfigurirana. Prosimo, sledite dokumentaciji." + }, + "create_entry": { + "default": "Uspe\u0161no preverjena s Home Connect." + }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/zh-Hant.json b/homeassistant/components/home_connect/translations/zh-Hant.json new file mode 100644 index 00000000000..5132dedd515 --- /dev/null +++ b/homeassistant/components/home_connect/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "missing_configuration": "Home Connect \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Home Connect \u8a2d\u5099\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 7a0ae33345a..e0a4d88ec6a 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -2,7 +2,6 @@ import asyncio import itertools as it import logging -from typing import Awaitable import voluptuous as vol @@ -33,7 +32,7 @@ SERVICE_SET_LOCATION = "set_location" SCHEMA_UPDATE_ENTITY = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) -async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: """Set up general services related to Home Assistant.""" async def async_handle_turn_service(service): diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 774c815a5c2..d8a4c453015 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Home Assistent" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b2aeea85c94..0a208e012fb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -1,4 +1,5 @@ """Support for Apple HomeKit.""" +import asyncio import ipaddress import logging @@ -6,87 +7,86 @@ from aiohttp import web import voluptuous as vol from zeroconf import InterfaceChoice -from homeassistant.components import cover, vacuum +from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING -from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SERVICE, - ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, - CONF_TYPE, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) -from homeassistant.core import callback -from homeassistant.exceptions import Unauthorized -from homeassistant.helpers import entity_registry +from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized +from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.entityfilter import ( + BASE_FILTER_SCHEMA, + CONF_EXCLUDE_DOMAINS, + CONF_EXCLUDE_ENTITIES, + CONF_INCLUDE_DOMAINS, + CONF_INCLUDE_ENTITIES, + convert_filter, +) +from homeassistant.loader import async_get_integration from homeassistant.util import get_local_ip -from homeassistant.util.decorator import Registry +from .accessories import get_accessory from .aidmanager import AccessoryAidStorage from .const import ( AID_STORAGE, ATTR_DISPLAY_NAME, + ATTR_INTERGRATION, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, ATTR_VALUE, BRIDGE_NAME, CONF_ADVERTISE_IP, CONF_AUTO_START, CONF_ENTITY_CONFIG, - CONF_FEATURE_LIST, + CONF_ENTRY_INDEX, CONF_FILTER, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, CONF_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE, + CONFIG_OPTIONS, DEFAULT_AUTO_START, DEFAULT_PORT, DEFAULT_SAFE_MODE, DEFAULT_ZEROCONF_DEFAULT_INTERFACE, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_PM25, DOMAIN, EVENT_HOMEKIT_CHANGED, - HOMEKIT_FILE, + HOMEKIT, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, + MANUFACTURER, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, - TYPE_FAUCET, - TYPE_OUTLET, - TYPE_SHOWER, - TYPE_SPRINKLER, - TYPE_SWITCH, - TYPE_VALVE, + SHUTDOWN_TIMEOUT, + UNDO_UPDATE_LISTENER, ) from .util import ( + dismiss_setup_message, + get_persist_fullpath_for_entry_id, + migrate_filesystem_state_data_for_primary_imported_entry_id, + port_is_available, + remove_state_files_for_entry_id, show_setup_message, validate_entity_config, - validate_media_player_features, ) _LOGGER = logging.getLogger(__name__) MAX_DEVICES = 150 -TYPES = Registry() # #### Driver Status #### STATUS_READY = 0 @@ -94,66 +94,140 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 -SWITCH_TYPES = { - TYPE_FAUCET: "Valve", - TYPE_OUTLET: "Outlet", - TYPE_SHOWER: "Valve", - TYPE_SPRINKLER: "Valve", - TYPE_SWITCH: "Switch", - TYPE_VALVE: "Valve", -} -CONFIG_SCHEMA = vol.Schema( +def _has_all_unique_names_and_ports(bridges): + """Validate that each homekit bridge configured has a unique name.""" + names = [bridge[CONF_NAME] for bridge in bridges] + ports = [bridge[CONF_PORT] for bridge in bridges] + vol.Schema(vol.Unique())(names) + vol.Schema(vol.Unique())(ports) + return bridges + + +BRIDGE_SCHEMA = vol.Schema( { - DOMAIN: vol.All( - { - vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( - cv.string, vol.Length(min=3, max=25) - ), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_ADVERTISE_IP): vol.All( - ipaddress.ip_address, cv.string - ), - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, - vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, - vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, - vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - vol.Optional( - CONF_ZEROCONF_DEFAULT_INTERFACE, - default=DEFAULT_ZEROCONF_DEFAULT_INTERFACE, - ): cv.boolean, - } - ) + vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( + cv.string, vol.Length(min=3, max=25) + ), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, + vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, + vol.Optional( + CONF_ZEROCONF_DEFAULT_INTERFACE, default=DEFAULT_ZEROCONF_DEFAULT_INTERFACE, + ): cv.boolean, }, extra=vol.ALLOW_EXTRA, ) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [BRIDGE_SCHEMA], _has_all_unique_names_and_ports)}, + extra=vol.ALLOW_EXTRA, +) + + RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema( {vol.Required(ATTR_ENTITY_ID): cv.entity_ids} ) -async def async_setup(hass, config): - """Set up the HomeKit component.""" - _LOGGER.debug("Begin setup HomeKit") +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the HomeKit from yaml.""" - aid_storage = hass.data[AID_STORAGE] = AccessoryAidStorage(hass) - await aid_storage.async_initialize() + hass.data.setdefault(DOMAIN, {}) - hass.http.register_view(HomeKitPairingQRView) + _async_register_events_and_services(hass) + + if DOMAIN not in config: + return True + + current_entries = hass.config_entries.async_entries(DOMAIN) + + entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} + + for index, conf in enumerate(config[DOMAIN]): + bridge_name = conf[CONF_NAME] + + if ( + bridge_name in entries_by_name + and entries_by_name[bridge_name].source == SOURCE_IMPORT + ): + entry = entries_by_name[bridge_name] + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + options[key] = data[key] + del data[key] + + hass.config_entries.async_update_entry(entry, data=data, options=options) + continue + + conf[CONF_ENTRY_INDEX] = index + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up HomeKit from a config entry.""" + _async_import_options_from_data_if_missing(hass, entry) + + conf = entry.data + options = entry.options - conf = config[DOMAIN] name = conf[CONF_NAME] port = conf[CONF_PORT] + _LOGGER.debug("Begin setup HomeKit for %s", name) + + # If the previous instance hasn't cleaned up yet + # we need to wait a bit + if not await hass.async_add_executor_job(port_is_available, port): + _LOGGER.warning("The local port %s is in use.", port) + raise ConfigEntryNotReady + + if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0: + _LOGGER.debug("Migrating legacy HomeKit data for %s", name) + hass.async_add_executor_job( + migrate_filesystem_state_data_for_primary_imported_entry_id, + hass, + entry.entry_id, + ) + + aid_storage = AccessoryAidStorage(hass, entry.entry_id) + + await aid_storage.async_initialize() + # ip_address and advertise_ip are yaml only ip_address = conf.get(CONF_IP_ADDRESS) advertise_ip = conf.get(CONF_ADVERTISE_IP) - auto_start = conf[CONF_AUTO_START] - safe_mode = conf[CONF_SAFE_MODE] - entity_filter = conf[CONF_FILTER] - entity_config = conf[CONF_ENTITY_CONFIG] + + entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy() + auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) + safe_mode = options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE) + entity_filter = convert_filter( + options.get( + CONF_FILTER, + { + CONF_INCLUDE_DOMAINS: [], + CONF_EXCLUDE_DOMAINS: [], + CONF_INCLUDE_ENTITIES: [], + CONF_EXCLUDE_ENTITIES: [], + }, + ) + ) interface_choice = ( - InterfaceChoice.Default if conf.get(CONF_ZEROCONF_DEFAULT_INTERFACE) else None + InterfaceChoice.Default + if options.get(CONF_ZEROCONF_DEFAULT_INTERFACE) + else None ) homekit = HomeKit( @@ -166,20 +240,101 @@ async def async_setup(hass, config): safe_mode, advertise_ip, interface_choice, + entry.entry_id, ) await hass.async_add_executor_job(homekit.setup) + await homekit.async_setup_zeroconf() + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + AID_STORAGE: aid_storage, + HOMEKIT: homekit, + UNDO_UPDATE_LISTENER: undo_listener, + } + + if hass.state == CoreState.running: + await homekit.async_start() + elif auto_start: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, homekit.async_start) + + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + if entry.source == SOURCE_IMPORT: + return + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + dismiss_setup_message(hass, entry.entry_id) + + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] + + if homekit.status == STATUS_RUNNING: + await homekit.async_stop() + + for _ in range(0, SHUTDOWN_TIMEOUT): + if not await hass.async_add_executor_job( + port_is_available, entry.data[CONF_PORT] + ): + _LOGGER.info("Waiting for the HomeKit server to shutdown.") + await asyncio.sleep(1) + + hass.data[DOMAIN].pop(entry.entry_id) + + return True + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): + """Remove a config entry.""" + return await hass.async_add_executor_job( + remove_state_files_for_entry_id, hass, entry.entry_id + ) + + +@callback +def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): + options = dict(entry.options) + data = dict(entry.data) + modified = False + for importable_option in CONFIG_OPTIONS: + if importable_option not in entry.options and importable_option in entry.data: + options[importable_option] = entry.data[importable_option] + del data[importable_option] + modified = True + + if modified: + hass.config_entries.async_update_entry(entry, data=data, options=options) + + +@callback +def _async_register_events_and_services(hass: HomeAssistant): + """Register events and services for HomeKit.""" + + hass.http.register_view(HomeKitPairingQRView) def handle_homekit_reset_accessory(service): """Handle start HomeKit service call.""" - if homekit.status != STATUS_RUNNING: - _LOGGER.warning( - "HomeKit is not running. Either it is waiting to be " - "started or has been stopped." - ) - return + for entry_id in hass.data[DOMAIN]: + if HOMEKIT not in hass.data[DOMAIN][entry_id]: + continue + homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + if homekit.status != STATUS_RUNNING: + _LOGGER.warning( + "HomeKit is not running. Either it is waiting to be " + "started or has been stopped." + ) + continue - entity_ids = service.data.get("entity_id") - homekit.reset_accessories(entity_ids) + entity_ids = service.data.get("entity_id") + homekit.reset_accessories(entity_ids) hass.services.async_register( DOMAIN, @@ -208,123 +363,24 @@ async def async_setup(hass, config): DOMAIN, EVENT_HOMEKIT_CHANGED, async_describe_logbook_event ) - if auto_start: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.async_start) - return True - async def async_handle_homekit_service_start(service): """Handle start HomeKit service call.""" - if homekit.status != STATUS_READY: - _LOGGER.warning( - "HomeKit is not ready. Either it is already running or has " - "been stopped." - ) - return - await homekit.async_start() + for entry_id in hass.data[DOMAIN]: + if HOMEKIT not in hass.data[DOMAIN][entry_id]: + continue + homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + if homekit.status != STATUS_READY: + _LOGGER.warning( + "HomeKit is not ready. Either it is already running or has " + "been stopped." + ) + continue + await homekit.async_start() hass.services.async_register( DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start ) - return True - - -def get_accessory(hass, driver, state, aid, config): - """Take state and return an accessory object if supported.""" - if not aid: - _LOGGER.warning( - 'The entity "%s" is not supported, since it ' - "generates an invalid aid, please change it.", - state.entity_id, - ) - return None - - a_type = None - name = config.get(CONF_NAME, state.name) - - if state.domain == "alarm_control_panel": - a_type = "SecuritySystem" - - elif state.domain in ("binary_sensor", "device_tracker", "person"): - a_type = "BinarySensor" - - elif state.domain == "climate": - a_type = "Thermostat" - - elif state.domain == "cover": - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( - cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE - ): - a_type = "GarageDoorOpener" - elif features & cover.SUPPORT_SET_POSITION: - a_type = "WindowCovering" - elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): - a_type = "WindowCoveringBasic" - - elif state.domain == "fan": - a_type = "Fan" - - elif state.domain == "light": - a_type = "Light" - - elif state.domain == "lock": - a_type = "Lock" - - elif state.domain == "media_player": - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - feature_list = config.get(CONF_FEATURE_LIST, []) - - if device_class == DEVICE_CLASS_TV: - a_type = "TelevisionMediaPlayer" - elif validate_media_player_features(state, feature_list): - a_type = "MediaPlayer" - - elif state.domain == "sensor": - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - if device_class == DEVICE_CLASS_TEMPERATURE or unit in ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - ): - a_type = "TemperatureSensor" - elif device_class == DEVICE_CLASS_HUMIDITY and unit == UNIT_PERCENTAGE: - a_type = "HumiditySensor" - elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: - a_type = "AirQualitySensor" - elif device_class == DEVICE_CLASS_CO: - a_type = "CarbonMonoxideSensor" - elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: - a_type = "CarbonDioxideSensor" - elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"): - a_type = "LightSensor" - - elif state.domain == "switch": - switch_type = config.get(CONF_TYPE, TYPE_SWITCH) - a_type = SWITCH_TYPES[switch_type] - - elif state.domain == "vacuum": - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME): - a_type = "DockVacuum" - else: - a_type = "Switch" - - elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): - a_type = "Switch" - - elif state.domain == "water_heater": - a_type = "WaterHeater" - - if a_type is None: - return None - - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) - class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" @@ -340,6 +396,7 @@ class HomeKit: safe_mode, advertise_ip=None, interface_choice=None, + entry_id=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -351,6 +408,7 @@ class HomeKit: self._safe_mode = safe_mode self._advertise_ip = advertise_ip self._interface_choice = interface_choice + self._entry_id = entry_id self.status = STATUS_READY self.bridge = None @@ -362,25 +420,34 @@ class HomeKit: from .accessories import HomeBridge, HomeDriver self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - ip_addr = self._ip_address or get_local_ip() - path = self.hass.config.path(HOMEKIT_FILE) + persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) + self.driver = HomeDriver( self.hass, + self._entry_id, + self._name, address=ip_addr, port=self._port, - persist_file=path, + persist_file=persist_file, advertised_address=self._advertise_ip, interface_choice=self._interface_choice, ) + self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: - _LOGGER.debug("Safe_mode selected") + _LOGGER.debug("Safe_mode selected for %s", self._name) self.driver.safe_mode = True + async def async_setup_zeroconf(self): + """Share the system zeroconf instance.""" + # Replace the existing zeroconf instance. + await self.hass.async_add_executor_job(self.driver.advertiser.close) + self.driver.advertiser = await zeroconf.async_get_instance(self.hass) + def reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" - aid_storage = self.hass.data[AID_STORAGE] + aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE] removed = [] for entity_id in entity_ids: aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) @@ -411,9 +478,9 @@ class HomeKit: ) return - aid = self.hass.data[AID_STORAGE].get_or_allocate_aid_for_entity_id( - state.entity_id - ) + aid = self.hass.data[DOMAIN][self._entry_id][ + AID_STORAGE + ].get_or_allocate_aid_for_entity_id(state.entity_id) conf = self._config.pop(state.entity_id, {}) # If an accessory cannot be created or added due to an exception # of any kind (usually in pyhap) it should not prevent @@ -436,11 +503,13 @@ class HomeKit: async def async_start(self, *args): """Start the accessory driver.""" + if self.status != STATUS_READY: return self.status = STATUS_WAIT ent_reg = await entity_registry.async_get_registry(self.hass) + dev_reg = await device_registry.async_get_registry(self.hass) device_lookup = ent_reg.async_get_device_class_lookup( { @@ -454,13 +523,36 @@ class HomeKit: if not self._filter(state.entity_id): continue - self._async_configure_linked_battery_sensors(ent_reg, device_lookup, state) + ent_reg_ent = ent_reg.async_get(state.entity_id) + if ent_reg_ent: + await self._async_set_device_info_attributes( + ent_reg_ent, dev_reg, state.entity_id + ) + self._async_configure_linked_battery_sensors( + ent_reg_ent, device_lookup, state + ) + bridged_states.append(state) + self._async_register_bridge(dev_reg) await self.hass.async_add_executor_job(self._start, bridged_states) + @callback + def _async_register_bridge(self, dev_reg): + """Register the bridge as a device so homekit_controller and exclude it from discovery.""" + dev_reg.async_get_or_create( + config_entry_id=self._entry_id, + connections={ + (device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac) + }, + manufacturer=MANUFACTURER, + name=self._name, + model="Home Assistant HomeKit Bridge", + ) + def _start(self, bridged_states): from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel + type_cameras, type_covers, type_fans, type_lights, @@ -479,11 +571,15 @@ class HomeKit: if not self.driver.state.paired: show_setup_message( - self.hass, self.driver.state.pincode, self.bridge.xhm_uri() + self.hass, + self._entry_id, + self._name, + self.driver.state.pincode, + self.bridge.xhm_uri(), ) - _LOGGER.debug("Driver start") - self.hass.async_add_executor_job(self.driver.start) + _LOGGER.debug("Driver start for %s", self._name) + self.hass.add_job(self.driver.start) self.status = STATUS_RUNNING async def async_stop(self, *args): @@ -491,26 +587,25 @@ class HomeKit: if self.status != STATUS_RUNNING: return self.status = STATUS_STOPPED - - _LOGGER.debug("Driver stop") - self.hass.async_add_executor_job(self.driver.stop) + _LOGGER.debug("Driver stop for %s", self._name) + self.hass.add_job(self.driver.stop) @callback - def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state): - entry = ent_reg.async_get(state.entity_id) - + def _async_configure_linked_battery_sensors( + self, ent_reg_ent, device_lookup, state + ): if ( - entry is None - or entry.device_id is None - or entry.device_id not in device_lookup - or entry.device_class + ent_reg_ent is None + or ent_reg_ent.device_id is None + or ent_reg_ent.device_id not in device_lookup + or ent_reg_ent.device_class in (DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY) ): return if ATTR_BATTERY_CHARGING not in state.attributes: battery_charging_binary_sensor_entity_id = device_lookup[ - entry.device_id + ent_reg_ent.device_id ].get(("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING)) if battery_charging_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( @@ -519,7 +614,7 @@ class HomeKit: ) if ATTR_BATTERY_LEVEL not in state.attributes: - battery_sensor_entity_id = device_lookup[entry.device_id].get( + battery_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( ("sensor", DEVICE_CLASS_BATTERY) ) if battery_sensor_entity_id: @@ -527,6 +622,21 @@ class HomeKit: CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id ) + async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id): + """Set attributes that will be used for homekit device info.""" + ent_cfg = self._config.setdefault(entity_id, {}) + if ent_reg_ent.device_id: + dev_reg_ent = dev_reg.async_get(ent_reg_ent.device_id) + if dev_reg_ent.manufacturer: + ent_cfg[ATTR_MANUFACTURER] = dev_reg_ent.manufacturer + if dev_reg_ent.model: + ent_cfg[ATTR_MODEL] = dev_reg_ent.model + if dev_reg_ent.sw_version: + ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version + if ATTR_MANUFACTURER not in ent_cfg: + integration = await async_get_integration(self.hass, ent_reg_ent.platform) + ent_cfg[ATTR_INTERGRATION] = integration.name + class HomeKitPairingQRView(HomeAssistantView): """Display the homekit pairing code at a protected url.""" @@ -538,9 +648,17 @@ class HomeKitPairingQRView(HomeAssistantView): # pylint: disable=no-self-use async def get(self, request): """Retrieve the pairing QRCode image.""" - if request.query_string != request.app["hass"].data[HOMEKIT_PAIRING_QR_SECRET]: + if not request.query_string: + raise Unauthorized() + entry_id, secret = request.query_string.split("-") + + if ( + entry_id not in request.app["hass"].data[DOMAIN] + or secret + != request.app["hass"].data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR_SECRET] + ): raise Unauthorized() return web.Response( - body=request.app["hass"].data[HOMEKIT_PAIRING_QR], + body=request.app["hass"].data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR], content_type="image/svg+xml", ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4f0a840770c..3cd3c46613b 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,12 +8,26 @@ from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER +from homeassistant.components import cover, vacuum +from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SERVICE, + ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_TYPE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, STATE_ON, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UNIT_PERCENTAGE, __version__, ) from homeassistant.core import callback as ha_callback, split_entity_id @@ -22,30 +36,60 @@ from homeassistant.helpers.event import ( track_point_in_utc_time, ) from homeassistant.util import dt as dt_util +from homeassistant.util.decorator import Registry from .const import ( ATTR_DISPLAY_NAME, + ATTR_INTERGRATION, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, + CONF_FEATURE_LIST, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEBOUNCE_TIMEOUT, DEFAULT_LOW_BATTERY_THRESHOLD, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_PM25, EVENT_HOMEKIT_CHANGED, HK_CHARGING, HK_NOT_CHARGABLE, HK_NOT_CHARGING, MANUFACTURER, SERV_BATTERY_SERVICE, + TYPE_FAUCET, + TYPE_OUTLET, + TYPE_SHOWER, + TYPE_SPRINKLER, + TYPE_SWITCH, + TYPE_VALVE, +) +from .util import ( + convert_to_float, + dismiss_setup_message, + format_sw_version, + show_setup_message, + validate_media_player_features, ) -from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) +SWITCH_TYPES = { + TYPE_FAUCET: "Valve", + TYPE_OUTLET: "Outlet", + TYPE_SHOWER: "Valve", + TYPE_SPRINKLER: "Valve", + TYPE_SWITCH: "Switch", + TYPE_VALVE: "Valve", +} +TYPES = Registry() def debounce(func): @@ -79,23 +123,149 @@ def debounce(func): return wrapper +def get_accessory(hass, driver, state, aid, config): + """Take state and return an accessory object if supported.""" + if not aid: + _LOGGER.warning( + 'The entity "%s" is not supported, since it ' + "generates an invalid aid, please change it.", + state.entity_id, + ) + return None + + a_type = None + name = config.get(CONF_NAME, state.name) + + if state.domain == "alarm_control_panel": + a_type = "SecuritySystem" + + elif state.domain in ("binary_sensor", "device_tracker", "person"): + a_type = "BinarySensor" + + elif state.domain == "climate": + a_type = "Thermostat" + + elif state.domain == "cover": + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( + cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE + ): + a_type = "GarageDoorOpener" + elif features & cover.SUPPORT_SET_POSITION: + a_type = "WindowCovering" + elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): + a_type = "WindowCoveringBasic" + + elif state.domain == "fan": + a_type = "Fan" + + elif state.domain == "light": + a_type = "Light" + + elif state.domain == "lock": + a_type = "Lock" + + elif state.domain == "media_player": + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + feature_list = config.get(CONF_FEATURE_LIST, []) + + if device_class == DEVICE_CLASS_TV: + a_type = "TelevisionMediaPlayer" + elif validate_media_player_features(state, feature_list): + a_type = "MediaPlayer" + + elif state.domain == "sensor": + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if device_class == DEVICE_CLASS_TEMPERATURE or unit in ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + ): + a_type = "TemperatureSensor" + elif device_class == DEVICE_CLASS_HUMIDITY and unit == UNIT_PERCENTAGE: + a_type = "HumiditySensor" + elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: + a_type = "AirQualitySensor" + elif device_class == DEVICE_CLASS_CO: + a_type = "CarbonMonoxideSensor" + elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: + a_type = "CarbonDioxideSensor" + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"): + a_type = "LightSensor" + + elif state.domain == "switch": + switch_type = config.get(CONF_TYPE, TYPE_SWITCH) + a_type = SWITCH_TYPES[switch_type] + + elif state.domain == "vacuum": + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME): + a_type = "DockVacuum" + else: + a_type = "Switch" + + elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): + a_type = "Switch" + + elif state.domain == "water_heater": + a_type = "WaterHeater" + + elif state.domain == "camera": + a_type = "Camera" + + if a_type is None: + return None + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) + return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) + + class HomeAccessory(Accessory): """Adapter class for Accessory.""" def __init__( - self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER + self, + hass, + driver, + name, + entity_id, + aid, + config, + *args, + category=CATEGORY_OTHER, + **kwargs, ): """Initialize a Accessory object.""" - super().__init__(driver, name, aid=aid) - model = split_entity_id(entity_id)[0].replace("_", " ").title() + super().__init__(driver=driver, display_name=name, aid=aid, *args, **kwargs) + self.config = config or {} + domain = split_entity_id(entity_id)[0].replace("_", " ") + + if ATTR_MANUFACTURER in self.config: + manufacturer = self.config[ATTR_MANUFACTURER] + elif ATTR_INTERGRATION in self.config: + manufacturer = self.config[ATTR_INTERGRATION].replace("_", " ").title() + else: + manufacturer = f"{MANUFACTURER} {domain}".title() + if ATTR_MODEL in self.config: + model = self.config[ATTR_MODEL] + else: + model = domain.title() + if ATTR_SOFTWARE_VERSION in self.config: + sw_version = format_sw_version(self.config[ATTR_SOFTWARE_VERSION]) + else: + sw_version = __version__ + self.set_info_service( - firmware_revision=__version__, - manufacturer=MANUFACTURER, + manufacturer=manufacturer, model=model, serial_number=entity_id, + firmware_revision=sw_version, ) + self.category = category - self.config = config or {} self.entity_id = entity_id self.hass = hass self.debounce = {} @@ -165,8 +335,10 @@ class HomeAccessory(Accessory): Run inside the Home Assistant event loop. """ state = self.hass.states.get(self.entity_id) - self.hass.async_add_executor_job(self.update_state_callback, None, None, state) - async_track_state_change(self.hass, self.entity_id, self.update_state_callback) + self.async_update_state_callback(None, None, state) + async_track_state_change( + self.hass, self.entity_id, self.async_update_state_callback + ) battery_charging_state = None battery_state = None @@ -179,7 +351,9 @@ class HomeAccessory(Accessory): ATTR_BATTERY_CHARGING ) async_track_state_change( - self.hass, self.linked_battery_sensor, self.update_linked_battery + self.hass, + self.linked_battery_sensor, + self.async_update_linked_battery_callback, ) else: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) @@ -191,18 +365,18 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.linked_battery_charging_sensor, - self.update_linked_battery_charging, + self.async_update_linked_battery_charging_callback, ) elif battery_charging_state is None: battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: - self.hass.async_add_executor_job( - self.update_battery, battery_state, battery_charging_state - ) + self.async_update_battery(battery_state, battery_charging_state) @ha_callback - def update_state_callback(self, entity_id=None, old_state=None, new_state=None): + def async_update_state_callback( + self, entity_id=None, old_state=None, new_state=None + ): """Handle state change listener callback.""" _LOGGER.debug("New_state: %s", new_state) if new_state is None: @@ -220,32 +394,29 @@ class HomeAccessory(Accessory): ): battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: - self.hass.async_add_executor_job( - self.update_battery, battery_state, battery_charging_state - ) - self.hass.async_add_executor_job(self.update_state, new_state) + self.async_update_battery(battery_state, battery_charging_state) + self.async_update_state(new_state) @ha_callback - def update_linked_battery(self, entity_id=None, old_state=None, new_state=None): + def async_update_linked_battery_callback( + self, entity_id=None, old_state=None, new_state=None + ): """Handle linked battery sensor state change listener callback.""" if self.linked_battery_charging_sensor: battery_charging_state = None else: battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING) - self.hass.async_add_executor_job( - self.update_battery, new_state.state, battery_charging_state, - ) + self.async_update_battery(new_state.state, battery_charging_state) @ha_callback - def update_linked_battery_charging( + def async_update_linked_battery_charging_callback( self, entity_id=None, old_state=None, new_state=None ): """Handle linked battery charging sensor state change listener callback.""" - self.hass.async_add_executor_job( - self.update_battery, None, new_state.state == STATE_ON - ) + self.async_update_battery(None, new_state.state == STATE_ON) - def update_battery(self, battery_level, battery_charging): + @ha_callback + def async_update_battery(self, battery_level, battery_charging): """Update battery service if available. Only call this function if self._support_battery_level is True. @@ -276,7 +447,8 @@ class HomeAccessory(Accessory): "%s: Updated battery charging to %d", self.entity_id, hk_charging ) - def update_state(self, new_state): + @ha_callback + def async_update_state(self, new_state): """Handle state change to update HomeKit value. Overridden by accessory types. @@ -320,23 +492,43 @@ class HomeBridge(Bridge): def setup_message(self): """Prevent print of pyhap setup message to terminal.""" + def get_snapshot(self, info): + """Get snapshot from accessory if supported.""" + acc = self.accessories.get(info["aid"]) + if acc is None: + raise ValueError("Requested snapshot for missing accessory") + if not hasattr(acc, "get_snapshot"): + raise ValueError( + "Got a request for snapshot, but the Accessory " + 'does not define a "get_snapshot" method' + ) + return acc.get_snapshot(info) + class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, hass, **kwargs): + def __init__(self, hass, entry_id, bridge_name, **kwargs): """Initialize a AccessoryDriver object.""" super().__init__(**kwargs) self.hass = hass + self._entry_id = entry_id + self._bridge_name = bridge_name def pair(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" success = super().pair(client_uuid, client_public) if success: - dismiss_setup_message(self.hass) + dismiss_setup_message(self.hass, self._entry_id) return success def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" super().unpair(client_uuid) - show_setup_message(self.hass, self.state.pincode, self.accessory.xhm_uri()) + show_setup_message( + self.hass, + self._entry_id, + self._bridge_name, + self.state.pincode, + self.accessory.xhm_uri(), + ) diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 95181114e79..487865f22ab 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -15,13 +15,13 @@ from zlib import adler32 from fnvhash import fnv1a_32 +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.storage import Store -from .const import DOMAIN +from .util import get_aid_storage_filename_for_entry_id -AID_MANAGER_STORAGE_KEY = f"{DOMAIN}.aids" AID_MANAGER_STORAGE_VERSION = 1 AID_MANAGER_SAVE_DELAY = 2 @@ -74,13 +74,13 @@ class AccessoryAidStorage: persist over reboots. """ - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry): """Create a new entity map store.""" self.hass = hass - self.store = Store(hass, AID_MANAGER_STORAGE_VERSION, AID_MANAGER_STORAGE_KEY) self.allocations = {} self.allocated_aids = set() - + self._entry = entry + self.store = None self._entity_registry = None async def async_initialize(self): @@ -88,6 +88,8 @@ class AccessoryAidStorage: self._entity_registry = ( await self.hass.helpers.entity_registry.async_get_registry() ) + aidstore = get_aid_storage_filename_for_entry_id(self._entry) + self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore) raw_storage = await self.store.async_load() if not raw_storage: diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py new file mode 100644 index 00000000000..1dde5be9e98 --- /dev/null +++ b/homeassistant/components/homekit/config_flow.py @@ -0,0 +1,350 @@ +"""Config flow for HomeKit integration.""" +import logging +import random +import string + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.core import callback, split_entity_id +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import ( + CONF_EXCLUDE_DOMAINS, + CONF_EXCLUDE_ENTITIES, + CONF_INCLUDE_DOMAINS, + CONF_INCLUDE_ENTITIES, +) + +from .const import ( + CONF_AUTO_START, + CONF_ENTITY_CONFIG, + CONF_FILTER, + CONF_SAFE_MODE, + CONF_VIDEO_CODEC, + CONF_ZEROCONF_DEFAULT_INTERFACE, + DEFAULT_AUTO_START, + DEFAULT_CONFIG_FLOW_PORT, + DEFAULT_SAFE_MODE, + DEFAULT_ZEROCONF_DEFAULT_INTERFACE, + SHORT_BRIDGE_NAME, + VIDEO_CODEC_COPY, +) +from .const import DOMAIN # pylint:disable=unused-import +from .util import find_next_available_port + +_LOGGER = logging.getLogger(__name__) + +CONF_CAMERA_COPY = "camera_copy" +CONF_DOMAINS = "domains" + +SUPPORTED_DOMAINS = [ + "alarm_control_panel", + "automation", + "binary_sensor", + "camera", + "climate", + "cover", + "demo", + "device_tracker", + "fan", + "input_boolean", + "light", + "lock", + "media_player", + "person", + "remote", + "scene", + "script", + "sensor", + "switch", + "vacuum", + "water_heater", +] + +DEFAULT_DOMAINS = [ + "alarm_control_panel", + "climate", + "cover", + "light", + "lock", + "media_player", + "switch", + "vacuum", + "water_heater", +] + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for HomeKit.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize config flow.""" + self.homekit_data = {} + self.entry_title = None + + async def async_step_pairing(self, user_input=None): + """Pairing instructions.""" + if user_input is not None: + return self.async_create_entry( + title=self.entry_title, data=self.homekit_data + ) + return self.async_show_form( + step_id="pairing", + description_placeholders={CONF_NAME: self.homekit_data[CONF_NAME]}, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + port = await self._async_available_port() + name = self._async_available_name() + title = f"{name}:{port}" + self.homekit_data = user_input.copy() + self.homekit_data[CONF_NAME] = name + self.homekit_data[CONF_PORT] = port + self.homekit_data[CONF_FILTER] = { + CONF_INCLUDE_DOMAINS: user_input[CONF_INCLUDE_DOMAINS], + CONF_INCLUDE_ENTITIES: [], + CONF_EXCLUDE_DOMAINS: [], + CONF_EXCLUDE_ENTITIES: [], + } + del self.homekit_data[CONF_INCLUDE_DOMAINS] + self.entry_title = title + return await self.async_step_pairing() + + default_domains = [] if self._async_current_entries() else DEFAULT_DOMAINS + setup_schema = vol.Schema( + { + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool, + vol.Required( + CONF_INCLUDE_DOMAINS, default=default_domains + ): cv.multi_select(SUPPORTED_DOMAINS), + } + ) + + return self.async_show_form( + step_id="user", data_schema=setup_schema, errors=errors + ) + + async def async_step_import(self, user_input=None): + """Handle import from yaml.""" + if not self._async_is_unique_name_port(user_input): + return self.async_abort(reason="port_name_in_use") + return self.async_create_entry( + title=f"{user_input[CONF_NAME]}:{user_input[CONF_PORT]}", data=user_input + ) + + async def _async_available_port(self): + """Return an available port the bridge.""" + return await self.hass.async_add_executor_job( + find_next_available_port, DEFAULT_CONFIG_FLOW_PORT + ) + + @callback + def _async_available_name(self): + """Return an available for the bridge.""" + current_entries = self._async_current_entries() + + # We always pick a RANDOM name to avoid Zeroconf + # name collisions. If the name has been seen before + # pairing will probably fail. + acceptable_chars = string.ascii_uppercase + string.digits + trailer = "".join(random.choices(acceptable_chars, k=4)) + all_names = {entry.data[CONF_NAME] for entry in current_entries} + suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" + while suggested_name in all_names: + trailer = "".join(random.choices(acceptable_chars, k=4)) + suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" + + return suggested_name + + @callback + def _async_is_unique_name_port(self, user_input): + """Determine is a name or port is already used.""" + name = user_input[CONF_NAME] + port = user_input[CONF_PORT] + for entry in self._async_current_entries(): + if entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port: + return False + return True + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for tado.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + self.homekit_options = {} + self.included_cameras = set() + + async def async_step_yaml(self, user_input=None): + """No options for yaml managed entries.""" + if user_input is not None: + # Apparently not possible to abort an options flow + # at the moment + return self.async_create_entry(title="", data=self.config_entry.options) + + return self.async_show_form(step_id="yaml") + + async def async_step_advanced(self, user_input=None): + """Choose advanced options.""" + if user_input is not None: + self.homekit_options.update(user_input) + del self.homekit_options[CONF_INCLUDE_DOMAINS] + return self.async_create_entry(title="", data=self.homekit_options) + + schema_base = {} + + if self.show_advanced_options: + schema_base[ + vol.Optional( + CONF_AUTO_START, + default=self.homekit_options.get( + CONF_AUTO_START, DEFAULT_AUTO_START + ), + ) + ] = bool + else: + self.homekit_options[CONF_AUTO_START] = self.homekit_options.get( + CONF_AUTO_START, DEFAULT_AUTO_START + ) + + schema_base.update( + { + vol.Optional( + CONF_SAFE_MODE, + default=self.homekit_options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE), + ): bool, + vol.Optional( + CONF_ZEROCONF_DEFAULT_INTERFACE, + default=self.homekit_options.get( + CONF_ZEROCONF_DEFAULT_INTERFACE, + DEFAULT_ZEROCONF_DEFAULT_INTERFACE, + ), + ): bool, + } + ) + + return self.async_show_form( + step_id="advanced", data_schema=vol.Schema(schema_base) + ) + + async def async_step_cameras(self, user_input=None): + """Choose camera config.""" + if user_input is not None: + entity_config = self.homekit_options[CONF_ENTITY_CONFIG] + for entity_id in self.included_cameras: + if entity_id in user_input[CONF_CAMERA_COPY]: + entity_config.setdefault(entity_id, {})[ + CONF_VIDEO_CODEC + ] = VIDEO_CODEC_COPY + elif ( + entity_id in entity_config + and CONF_VIDEO_CODEC in entity_config[entity_id] + ): + del entity_config[entity_id][CONF_VIDEO_CODEC] + return await self.async_step_advanced() + + cameras_with_copy = [] + entity_config = self.homekit_options.setdefault(CONF_ENTITY_CONFIG, {}) + for entity in self.included_cameras: + hk_entity_config = entity_config.get(entity, {}) + if hk_entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY: + cameras_with_copy.append(entity) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_CAMERA_COPY, default=cameras_with_copy, + ): cv.multi_select(self.included_cameras), + } + ) + return self.async_show_form(step_id="cameras", data_schema=data_schema) + + async def async_step_exclude(self, user_input=None): + """Choose entities to exclude from the domain.""" + if user_input is not None: + self.homekit_options[CONF_FILTER] = { + CONF_INCLUDE_DOMAINS: self.homekit_options[CONF_INCLUDE_DOMAINS], + CONF_EXCLUDE_DOMAINS: self.homekit_options.get( + CONF_EXCLUDE_DOMAINS, [] + ), + CONF_INCLUDE_ENTITIES: self.homekit_options.get( + CONF_INCLUDE_ENTITIES, [] + ), + CONF_EXCLUDE_ENTITIES: user_input[CONF_EXCLUDE_ENTITIES], + } + for entity_id in user_input[CONF_EXCLUDE_ENTITIES]: + if entity_id in self.included_cameras: + self.included_cameras.remove(entity_id) + if self.included_cameras: + return await self.async_step_cameras() + return await self.async_step_advanced() + + entity_filter = self.homekit_options.get(CONF_FILTER, {}) + all_supported_entities = await self.hass.async_add_executor_job( + _get_entities_matching_domains, + self.hass, + self.homekit_options[CONF_INCLUDE_DOMAINS], + ) + self.included_cameras = { + entity_id + for entity_id in all_supported_entities + if entity_id.startswith("camera.") + } + data_schema = vol.Schema( + { + vol.Optional( + CONF_EXCLUDE_ENTITIES, + default=entity_filter.get(CONF_EXCLUDE_ENTITIES, []), + ): cv.multi_select(all_supported_entities), + } + ) + return self.async_show_form(step_id="exclude", data_schema=data_schema) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if self.config_entry.source == SOURCE_IMPORT: + return await self.async_step_yaml(user_input) + + if user_input is not None: + self.homekit_options.update(user_input) + return await self.async_step_exclude() + + self.homekit_options = dict(self.config_entry.options) + entity_filter = self.homekit_options.get(CONF_FILTER, {}) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_INCLUDE_DOMAINS, + default=entity_filter.get(CONF_INCLUDE_DOMAINS, []), + ): cv.multi_select(SUPPORTED_DOMAINS) + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +def _get_entities_matching_domains(hass, domains): + """List entities in the given domains.""" + included_domains = set(domains) + entity_ids = [ + state.entity_id + for state in hass.states.all() + if (split_entity_id(state.entity_id))[0] in included_domains + ] + entity_ids.sort() + return entity_ids diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index f0224ce71f4..3291fab7a30 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,20 +1,38 @@ """Constants used be the HomeKit component.""" + # #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DEVICE_PRECISION_LEEWAY = 6 DOMAIN = "homekit" HOMEKIT_FILE = ".homekit.state" -HOMEKIT_NOTIFY_ID = 4663548 AID_STORAGE = "homekit-aid-allocations" HOMEKIT_PAIRING_QR = "homekit-pairing-qr" HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" +HOMEKIT = "homekit" +UNDO_UPDATE_LISTENER = "undo_update_listener" +SHUTDOWN_TIMEOUT = 30 +CONF_ENTRY_INDEX = "index" + +# ### Codecs #### +VIDEO_CODEC_COPY = "copy" +VIDEO_CODEC_LIBX264 = "libx264" +AUDIO_CODEC_OPUS = "libopus" +VIDEO_CODEC_H264_OMX = "h264_omx" +AUDIO_CODEC_COPY = "copy" # #### Attributes #### ATTR_DISPLAY_NAME = "display_name" ATTR_VALUE = "value" +ATTR_INTERGRATION = "platform" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_SOFTWARE_VERSION = "sw_version" # #### Config #### CONF_ADVERTISE_IP = "advertise_ip" +CONF_AUDIO_CODEC = "audio_codec" +CONF_AUDIO_MAP = "audio_map" +CONF_AUDIO_PACKET_SIZE = "audio_packet_size" CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" @@ -23,15 +41,35 @@ CONF_FILTER = "filter" CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor" CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" +CONF_MAX_FPS = "max_fps" +CONF_MAX_HEIGHT = "max_height" +CONF_MAX_WIDTH = "max_width" CONF_SAFE_MODE = "safe_mode" CONF_ZEROCONF_DEFAULT_INTERFACE = "zeroconf_default_interface" +CONF_STREAM_ADDRESS = "stream_address" +CONF_STREAM_SOURCE = "stream_source" +CONF_SUPPORT_AUDIO = "support_audio" +CONF_VIDEO_CODEC = "video_codec" +CONF_VIDEO_MAP = "video_map" +CONF_VIDEO_PACKET_SIZE = "video_packet_size" # #### Config Defaults #### +DEFAULT_SUPPORT_AUDIO = False +DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS +DEFAULT_AUDIO_MAP = "0:a:0" +DEFAULT_AUDIO_PACKET_SIZE = 188 DEFAULT_AUTO_START = True DEFAULT_LOW_BATTERY_THRESHOLD = 20 +DEFAULT_MAX_FPS = 30 +DEFAULT_MAX_HEIGHT = 1080 +DEFAULT_MAX_WIDTH = 1920 DEFAULT_PORT = 51827 +DEFAULT_CONFIG_FLOW_PORT = 51828 DEFAULT_SAFE_MODE = False DEFAULT_ZEROCONF_DEFAULT_INTERFACE = False +DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264 +DEFAULT_VIDEO_MAP = "0:v:0" +DEFAULT_VIDEO_PACKET_SIZE = 1316 # #### Features #### FEATURE_ON_OFF = "on_off" @@ -49,6 +87,7 @@ SERVICE_HOMEKIT_RESET_ACCESSORY = "reset_accessory" # #### String Constants #### BRIDGE_MODEL = "Bridge" BRIDGE_NAME = "Home Assistant Bridge" +SHORT_BRIDGE_NAME = "HASS Bridge" BRIDGE_SERIAL_NUMBER = "homekit.bridge" MANUFACTURER = "Home Assistant" @@ -64,6 +103,7 @@ TYPE_VALVE = "valve" SERV_ACCESSORY_INFO = "AccessoryInformation" SERV_AIR_QUALITY_SENSOR = "AirQualitySensor" SERV_BATTERY_SERVICE = "BatteryService" +SERV_CAMERA_RTP_STREAM_MANAGEMENT = "CameraRTPStreamManagement" SERV_CARBON_DIOXIDE_SENSOR = "CarbonDioxideSensor" SERV_CARBON_MONOXIDE_SENSOR = "CarbonMonoxideSensor" SERV_CONTACT_SENSOR = "ContactSensor" @@ -143,6 +183,7 @@ CHAR_SERIAL_NUMBER = "SerialNumber" CHAR_SLEEP_DISCOVER_MODE = "SleepDiscoveryMode" CHAR_SMOKE_DETECTED = "SmokeDetected" CHAR_STATUS_LOW_BATTERY = "StatusLowBattery" +CHAR_STREAMING_STRATUS = "StreamingStatus" CHAR_SWING_MODE = "SwingMode" CHAR_TARGET_DOOR_STATE = "TargetDoorState" CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" @@ -203,3 +244,12 @@ HK_POSITION_STOPPED = 2 HK_NOT_CHARGING = 0 HK_CHARGING = 1 HK_NOT_CHARGABLE = 2 + +# ### Config Options ### +CONFIG_OPTIONS = [ + CONF_FILTER, + CONF_AUTO_START, + CONF_ZEROCONF_DEFAULT_INTERFACE, + CONF_SAFE_MODE, + CONF_ENTITY_CONFIG, +] diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py new file mode 100644 index 00000000000..835b04558e6 --- /dev/null +++ b/homeassistant/components/homekit/img_util.py @@ -0,0 +1,62 @@ +"""Image processing for HomeKit component.""" + +import logging + +from turbojpeg import TurboJPEG + +SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] + +_LOGGER = logging.getLogger(__name__) + + +def scale_jpeg_camera_image(cam_image, width, height): + """Scale a camera image as close as possible to one of the supported scaling factors.""" + turbo_jpeg = TurboJPEGSingleton.instance() + if not turbo_jpeg: + return cam_image.content + + (current_width, current_height, _, _) = turbo_jpeg.decode_header(cam_image.content) + + if current_width <= width or current_height <= height: + return cam_image.content + + ratio = width / current_width + + scaling_factor = SUPPORTED_SCALING_FACTORS[-1] + for supported_sf in SUPPORTED_SCALING_FACTORS: + if ratio >= (supported_sf[0] / supported_sf[1]): + scaling_factor = supported_sf + break + + return turbo_jpeg.scale_with_quality( + cam_image.content, scaling_factor=scaling_factor, quality=75, + ) + + +class TurboJPEGSingleton: + """ + Load TurboJPEG only once. + + Ensures we do not log load failures each snapshot + since camera image fetches happen every few + seconds. + """ + + __instance = None + + @staticmethod + def instance(): + """Singleton for TurboJPEG.""" + if TurboJPEGSingleton.__instance is None: + TurboJPEGSingleton() + return TurboJPEGSingleton.__instance + + def __init__(self): + """Try to create TurboJPEG only once.""" + try: + TurboJPEGSingleton.__instance = TurboJPEG() + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "libturbojpeg is not installed, cameras may impact HomeKit performance." + ) + TurboJPEGSingleton.__instance = False diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 482cef57ca7..8f7382b5d85 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,8 +2,9 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.8.2","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"], - "dependencies": ["http"], - "after_dependencies": ["logbook"], - "codeowners": ["@bdraco"] + "requirements": ["HAP-python==2.8.4","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1","PyTurboJPEG==1.4.0"], + "dependencies": ["http", "camera", "ffmpeg"], + "after_dependencies": ["logbook", "zeroconf"], + "codeowners": ["@bdraco"], + "config_flow": true } diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json new file mode 100644 index 00000000000..131ecd8db4c --- /dev/null +++ b/homeassistant/components/homekit/strings.json @@ -0,0 +1,60 @@ +{ + "title": "HomeKit Bridge", + "options": { + "step": { + "yaml": { + "title": "Adjust HomeKit Bridge Options", + "description": "This entry is controlled via YAML" + }, + "init": { + "data": { + "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" + }, + "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title": "Select domains to bridge." + }, + "exclude": { + "data": { + "exclude_entities": "Entities to exclude" + }, + "description": "Choose the entities that you do NOT want to be bridged.", + "title": "Exclude entities in selected domains from bridge" + }, + "cameras": { + "data": { + "camera_copy": "Cameras that support native H.264 streams" + }, + "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", + "title": "Select camera video codec." + }, + "advanced": { + "data": { + "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", + "safe_mode": "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title": "Advanced Configuration" + } + } + }, + "config": { + "step": { + "user": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include" + }, + "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title": "Activate HomeKit Bridge" + }, + "pairing": { + "title": "Pair HomeKit Bridge", + "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + } + }, + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured." + } + } +} diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json new file mode 100644 index 00000000000..79d2139fedc --- /dev/null +++ b/homeassistant/components/homekit/translations/ca.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Ja hi ha un enlla\u00e7 configurat amb aquest nom o port." + }, + "step": { + "pairing": { + "description": "Tan aviat com l'enlla\u00e7 {name} estigui llest, rebr\u00e0s una \"Notificaci\u00f3\" de configuraci\u00f3 de l'enlla\u00e7 HomeKit informan-te de que la vinculaci\u00f3 est\u00e0 disponible.", + "title": "Vinculaci\u00f3 de l'enlla\u00e7 HomeKit" + }, + "user": { + "data": { + "auto_start": "Autoarrencada (desactiva-ho si fas servir Z-Wave o algun altre sistema d'inici lent)", + "include_domains": "Dominis a incloure" + }, + "description": "L'enlla\u00e7 HomeKit et permet accedir a les teves entitats de Home Assistant directament a HomeKit. Aquests enll\u00e7os estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat per l'enlla\u00e7 prinipal nom\u00e9s est\u00e0 disponible amb YAML.", + "title": "Activaci\u00f3 de l'enlla\u00e7 HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "[%key::component::homekit::config::step::user::data::auto_start%]", + "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)", + "zeroconf_default_interface": "Utilitza la interf\u00edcie zeroconf predeterminada. Activa-ho si no es pot trobar l'enlla\u00e7 a l'aplicaci\u00f3 Casa (Home app)." + }, + "description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si l'enlla\u00e7 HomeKit no \u00e9s funcional.", + "title": "Configuraci\u00f3 avan\u00e7ada" + }, + "cameras": { + "data": { + "camera_copy": "C\u00e0meres que admeten fluxos H.264 natius" + }, + "title": "Selecci\u00f3 del c\u00f2dec de v\u00eddeo de c\u00e0mera" + }, + "exclude": { + "data": { + "exclude_entities": "Entitats a excloure" + }, + "description": "Selecciona les entitats que NO vulguis que siguin enlla\u00e7ades.", + "title": "Exclusi\u00f3 d'entitats de l'enlla\u00e7 en dominis seleccionats" + }, + "init": { + "data": { + "include_domains": "[%key::component::homekit::config::step::user::data::include_domains%]" + }, + "description": "Les entitats a \"Dominis a incloure\" s'enlla\u00e7aran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols excloure d'aquesta llista.", + "title": "Selecci\u00f3 dels dominis a enlla\u00e7ar." + }, + "yaml": { + "description": "Aquesta entrada es controla en YAML", + "title": "Ajusta les opcions de l'enlla\u00e7 HomeKit" + } + } + }, + "title": "Enlla\u00e7 HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json new file mode 100644 index 00000000000..3e96cd44af8 --- /dev/null +++ b/homeassistant/components/homekit/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json new file mode 100644 index 00000000000..70774806cf8 --- /dev/null +++ b/homeassistant/components/homekit/translations/de.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured.\nEine HomeKit Bridge mit dem selben Namen oder Port ist bereits vorhanden" + }, + "step": { + "pairing": { + "title": "HomeKit Bridge verbinden\n" + }, + "user": { + "data": { + "include_domains": "Einzubeziehende Domains" + }, + "title": "HomeKit Bridge aktivieren\n" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)" + }, + "title": "Erweiterte Konfiguration" + }, + "cameras": { + "data": { + "camera_copy": "Kameras, die native H.264-Streams unterst\u00fctzen" + }, + "title": "W\u00e4hlen Sie den Kamera-Video-Codec." + }, + "exclude": { + "data": { + "exclude_entities": "Auszuschlie\u00dfende Entit\u00e4ten" + } + }, + "init": { + "title": "W\u00e4hlen Sie die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus." + }, + "yaml": { + "description": "Dieser Eintrag wird \u00fcber YAML gesteuert", + "title": "Passen Sie die HomeKit Bridge-Optionen an" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json new file mode 100644 index 00000000000..1fc5ae59a0c --- /dev/null +++ b/homeassistant/components/homekit/translations/en.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured." + }, + "step": { + "pairing": { + "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", + "title": "Pair HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include" + }, + "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title": "Activate HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "safe_mode": "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title": "Advanced Configuration" + }, + "cameras": { + "data": { + "camera_copy": "Cameras that support native H.264 streams" + }, + "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", + "title": "Select camera video codec." + }, + "exclude": { + "data": { + "exclude_entities": "Entities to exclude" + }, + "description": "Choose the entities that you do NOT want to be bridged.", + "title": "Exclude entities in selected domains from bridge" + }, + "init": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title": "Select domains to bridge." + }, + "yaml": { + "description": "This entry is controlled via YAML", + "title": "Adjust HomeKit Bridge Options" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es-419.json b/homeassistant/components/homekit/translations/es-419.json new file mode 100644 index 00000000000..06c66697fbb --- /dev/null +++ b/homeassistant/components/homekit/translations/es-419.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Un puente con el mismo nombre o puerto ya est\u00e1 configurado." + }, + "step": { + "pairing": { + "description": "Tan pronto como el puente {name} est\u00e9 listo, el emparejamiento estar\u00e1 disponible en \"Notificaciones\" como \"Configuraci\u00f3n del puente HomeKit\".", + "title": "Emparejar HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", + "include_domains": "Dominios para incluir" + }, + "description": "Un HomeKit Bridge le permitir\u00e1 acceder a sus entidades de Home Assistant en HomeKit. Los puentes HomeKit est\u00e1n limitados a 150 accesorios por instancia, incluido el puente mismo. Si desea unir m\u00e1s de la cantidad m\u00e1xima de accesorios, se recomienda que use m\u00faltiples puentes HomeKit para diferentes dominios. La configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible a trav\u00e9s de YAML para el puente primario.", + "title": "Activar HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", + "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)", + "zeroconf_default_interface": "Use la interfaz zeroconf predeterminada (habil\u00edtela si no se puede encontrar el puente en la aplicaci\u00f3n Inicio)" + }, + "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", + "title": "Configuraci\u00f3n avanzada" + }, + "cameras": { + "data": { + "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" + }, + "description": "Verifique todas las c\u00e1maras que admiten transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en computadoras de placa \u00fanica.", + "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." + }, + "exclude": { + "data": { + "exclude_entities": "Entidades a excluir" + }, + "description": "Seleccione las entidades que NO desea puentear.", + "title": "Excluir entidades en dominios seleccionados del puente" + }, + "init": { + "data": { + "include_domains": "Dominios para incluir" + }, + "description": "Las entidades en los \"Dominios para incluir\" se vincular\u00e1n a HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", + "title": "Seleccione dominios para puentear." + }, + "yaml": { + "description": "Esta entrada se controla a trav\u00e9s de YAML", + "title": "Ajuste las opciones de puente de HomeKit" + } + } + }, + "title": "Puente HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json new file mode 100644 index 00000000000..92f64c435de --- /dev/null +++ b/homeassistant/components/homekit/translations/es.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Ya est\u00e1 configurada una pasarela con el mismo nombre o puerto." + }, + "step": { + "pairing": { + "description": "Tan pronto como la pasarela {name} est\u00e9 lista, la vinculaci\u00f3n estar\u00e1 disponible en \"Notificaciones\" como \"configuraci\u00f3n de pasarela Homekit\"", + "title": "Vincular pasarela Homekit" + }, + "user": { + "data": { + "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", + "include_domains": "Dominios para incluir" + }, + "description": "Una pasarela Homekit permitir\u00e1 a Homekit acceder a sus entidades de Home Assistant. La pasarela Homekit est\u00e1 limitada a 150 accesorios por instancia incluyendo la propia pasarela. Si desea enlazar m\u00e1s del m\u00e1ximo n\u00famero de accesorios, se recomienda que use multiples pasarelas Homekit para diferentes dominios. Configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible via YAML para la pasarela primaria.", + "title": "Activar pasarela Homekit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", + "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)", + "zeroconf_default_interface": "Use la interfaz zeroconf predeterminada (habil\u00edtela si no se puede encontrar el puente en la aplicaci\u00f3n Inicio)" + }, + "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", + "title": "Configuraci\u00f3n avanzada" + }, + "cameras": { + "data": { + "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" + }, + "description": "Verifique todas las c\u00e1maras que admiten transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", + "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." + }, + "exclude": { + "data": { + "exclude_entities": "Entidades a excluir" + }, + "description": "Elija las entidades que NO desea puentear.", + "title": "Excluir entidades en dominios seleccionados de bridge" + }, + "init": { + "data": { + "include_domains": "Dominios para incluir" + }, + "description": "Las entidades de los \"Dominios que se van a incluir\" se establecer\u00e1n en HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", + "title": "Seleccione los dominios que desea establecer un puente." + }, + "yaml": { + "description": "Esta entrada se controla a trav\u00e9s de YAML", + "title": "Ajustar las opciones del puente HomeKit" + } + } + }, + "title": "Pasarela Homekit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/fi.json b/homeassistant/components/homekit/translations/fi.json new file mode 100644 index 00000000000..a19444d73f1 --- /dev/null +++ b/homeassistant/components/homekit/translations/fi.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "pairing": { + "title": "Parita HomeKit-silta" + } + } + }, + "options": { + "step": { + "advanced": { + "title": "Lis\u00e4asetukset" + } + } + }, + "title": "HomeKit-silta" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json new file mode 100644 index 00000000000..17cc5149654 --- /dev/null +++ b/homeassistant/components/homekit/translations/fr.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Une passerelle avec le m\u00eame nom ou port est d\u00e9j\u00e0 configur\u00e9e." + }, + "step": { + "pairing": { + "description": "D\u00e8s que le pont {name} est pr\u00eat, l'appairage sera disponible dans \"Notifications\" sous \"Configuration de la Passerelle HomeKit\".", + "title": "Appairage de la Passerelle Homekit" + }, + "user": { + "data": { + "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", + "include_domains": "Domaines \u00e0 inclure" + }, + "description": "La passerelle HomeKit vous permettra d'acc\u00e9der \u00e0 vos entit\u00e9s Home Assistant dans HomeKit. Les passerelles HomeKit sont limit\u00e9es \u00e0 150 accessoires par instance, y compris la passerelle elle-m\u00eame. Si vous souhaitez connecter plus que le nombre maximum d'accessoires, il est recommand\u00e9 d'utiliser plusieurs passerelles HomeKit pour diff\u00e9rents domaines. La configuration d\u00e9taill\u00e9e des entit\u00e9s est uniquement disponible via YAML pour la passerelle principale.", + "title": "Activer la Passerelle HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", + "safe_mode": "Mode sans \u00e9chec (activez uniquement si le jumelage \u00e9choue)", + "zeroconf_default_interface": "Utiliser l'interface zeroconf par d\u00e9faut (activer si le pont est introuvable dans l'application Home)" + }, + "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", + "title": "Configuration avanc\u00e9e" + }, + "cameras": { + "data": { + "camera_copy": "Cam\u00e9ras prenant en charge les flux H.264 natifs" + }, + "title": "S\u00e9lectionnez le codec vid\u00e9o de la cam\u00e9ra." + }, + "exclude": { + "data": { + "exclude_entities": "Entit\u00e9s \u00e0 exclure" + }, + "description": "Choisissez les entit\u00e9s que vous ne souhaitez PAS voir reli\u00e9es.", + "title": "Exclure les entit\u00e9s des domaines s\u00e9lectionn\u00e9s de la passerelle" + }, + "init": { + "data": { + "include_domains": "Domaine \u00e0 inclure" + }, + "description": "Les entit\u00e9s des \u00abdomaines \u00e0 inclure\u00bb seront pont\u00e9es vers HomeKit. Vous pourrez s\u00e9lectionner les entit\u00e9s \u00e0 exclure de cette liste sur l'\u00e9cran suivant.", + "title": "S\u00e9lectionnez les domaines \u00e0 relier." + }, + "yaml": { + "description": "Cette entr\u00e9e est contr\u00f4l\u00e9e via YAML", + "title": "Ajuster les options de la passerelle HomeKit" + } + } + }, + "title": "Passerelle HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json new file mode 100644 index 00000000000..87ad743dca5 --- /dev/null +++ b/homeassistant/components/homekit/translations/he.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "yaml": { + "description": "\u05d9\u05e9\u05d5\u05ea \u05d6\u05d5 \u05e0\u05e9\u05dc\u05d8\u05ea \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea YAML" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json new file mode 100644 index 00000000000..6ebd08c88f3 --- /dev/null +++ b/homeassistant/components/homekit/translations/it.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Un bridge con lo stesso nome o porta \u00e8 gi\u00e0 configurato." + }, + "step": { + "pairing": { + "description": "Non appena il bridge {name} \u00e8 pronto, l'associazione sar\u00e0 disponibile in \"Notifiche\" come \"HomeKit Bridge Setup\".", + "title": "Associa HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", + "include_domains": "Domini da includere" + }, + "description": "Un HomeKit Bridge ti permetter\u00e0 di accedere alle tue entit\u00e0 Home Assistant in HomeKit. Gli HomeKit Bridges sono limitati a 150 accessori per istanza, incluso il bridge stesso. Se si desidera collegare un numero di accessori superiore al massimo consentito, si consiglia di utilizzare pi\u00f9 HomeKit Bridge per domini diversi. La configurazione dettagliata delle entit\u00e0 \u00e8 disponibile solo tramite YAML per il bridge primario.", + "title": "Attiva HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", + "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)", + "zeroconf_default_interface": "Utilizzare l'interfaccia zeroconf predefinita (abilitare se il bridge non pu\u00f2 essere trovato nell'app Home)" + }, + "description": "Queste impostazioni devono essere modificate solo se il bridge HomeKit non funziona.", + "title": "Configurazione Avanzata" + }, + "cameras": { + "data": { + "camera_copy": "Telecamere che supportano flussi H.264 nativi" + }, + "description": "Controllare tutte le telecamere che supportano i flussi H.264 nativi. Se la videocamera non emette uno stream H.264, il sistema provveder\u00e0 a transcodificare il video in H.264 per HomeKit. La transcodifica richiede una CPU performante ed \u00e8 improbabile che funzioni su computer a scheda singola.", + "title": "Seleziona il codec video della videocamera." + }, + "exclude": { + "data": { + "exclude_entities": "Entit\u00e0 da escludere" + }, + "description": "Scegliere le entit\u00e0 che NON si desidera collegare.", + "title": "Escludere le entit\u00e0 dal bridge nei domini selezionati " + }, + "init": { + "data": { + "include_domains": "Domini da includere" + }, + "description": "Le entit\u00e0 nei \"Domini da includere\" saranno collegate a HomeKit. Sarai in grado di selezionare quali entit\u00e0 escludere da questo elenco nella schermata successiva.", + "title": "Selezionare i domini al bridge." + }, + "yaml": { + "description": "Questa voce \u00e8 controllata tramite YAML", + "title": "Regolare le opzioni di HomeKit Bridge" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json new file mode 100644 index 00000000000..52b329104df --- /dev/null +++ b/homeassistant/components/homekit/translations/ko.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "pairing": { + "description": "{name} \ube0c\ub9ac\uc9c0\uac00 \uc900\ube44\ub418\uba74 \"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit \ube0c\ub9ac\uc9c0 \uc124\uc815\"\uc73c\ub85c \ud398\uc5b4\ub9c1\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud398\uc5b4\ub9c1\ud558\uae30" + }, + "user": { + "data": { + "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", + "include_domains": "\ud3ec\ud568 \ud560 \ub3c4\uba54\uc778" + }, + "description": "HomeKit \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\uba74 HomeKit \uc5d0\uc11c Home Assistant \uad6c\uc131\uc694\uc18c\uc5d0 \uc561\uc138\uc2a4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. HomeKit \ube0c\ub9ac\uc9c0\ub294 \ube0c\ub9ac\uc9c0 \uc790\uccb4\ub97c \ud3ec\ud568\ud558\uc5ec \uc778\uc2a4\ud134\uc2a4\ub2f9 150\uac1c\uc758 \uc561\uc138\uc11c\ub9ac\ub85c \uc81c\ud55c\ub429\ub2c8\ub2e4. \ucd5c\ub300 \uc561\uc138\uc11c\ub9ac \uc218\ub97c \ucd08\uacfc\ud558\uc5ec \ube0c\ub9ac\uc9d5\ud558\ub824\uba74 \uc5ec\ub7ec \ub3c4\uba54\uc778\uc5d0 \ub300\ud574 \uc5ec\ub7ec \uac1c\uc758 \ud648\ud0b7 \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \uae30\ubcf8 \ube0c\ub9ac\uc9c0\uc758 YAML \uc744 \ud1b5\ud574\uc11c\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud65c\uc131\ud654\ud558\uae30" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", + "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", + "zeroconf_default_interface": "\uae30\ubcf8 zeroconf \uc778\ud130\ud398\uc774\uc2a4 \uc0ac\uc6a9 (Home \uc571\uc5d0\uc11c \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218\uc5c6\ub294 \uacbd\uc6b0 \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" + }, + "description": "\uc774 \uc124\uc815\uc740 HomeKit \ube0c\ub9ac\uc9c0\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc870\uc815\ud558\uba74 \ub429\ub2c8\ub2e4.", + "title": "\uace0\uae09 \uad6c\uc131\ud558\uae30" + }, + "cameras": { + "data": { + "camera_copy": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \uce74\uba54\ub77c" + }, + "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 \ub77c\uc988\ubca0\ub9ac\ud30c\uc774\uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30" + }, + "exclude": { + "data": { + "exclude_entities": "\uc81c\uc678 \ud560 \uad6c\uc131\uc694\uc18c" + }, + "description": "\ube0c\ub9ac\uc9c0\ud558\uc9c0 \uc54a\uc73c\ub824\ub294 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\ube0c\ub9ac\uc9c0\uc5d0\uc11c \uc120\ud0dd\ud55c \ub3c4\uba54\uc778\uc758 \uad6c\uc131\uc694\uc18c \uc81c\uc678\ud558\uae30" + }, + "init": { + "data": { + "include_domains": "\ud3ec\ud568 \ud560 \ub3c4\uba54\uc778" + }, + "description": "\"\ud3ec\ud568 \ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \uc5f0\uacb0\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc758 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\ube0c\ub9ac\uc9c0 \ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" + }, + "yaml": { + "description": "\uc774 \ud56d\ubaa9\uc740 YAML \uc744 \ud1b5\ud574 \uc81c\uc5b4\ub429\ub2c8\ub2e4", + "title": "HomeKit \ube0c\ub9ac\uc9c0 \uc635\uc158 \uc870\uc815\ud558\uae30" + } + } + }, + "title": "HomeKit \ube0c\ub9ac\uc9c0" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/lb.json b/homeassistant/components/homekit/translations/lb.json new file mode 100644 index 00000000000..85a578ec567 --- /dev/null +++ b/homeassistant/components/homekit/translations/lb.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Eng Bridge mat d\u00ebsem Numm oder Port ass sch konfigur\u00e9iert." + }, + "step": { + "pairing": { + "description": "Soubaal {name} bridge prett ass, ass Kopplung disponibel an den 'Notifikatioune' als \"HomeKit Bridge Setup\".", + "title": "Pair Homekit Bridge" + }, + "user": { + "data": { + "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)", + "include_domains": "Domaine d\u00e9i solle abegraff ginn." + }, + "description": "Eng HomeKit Bridge erlaabt et Home Assistant Entit\u00e9iten am HomeKit z'acc\u00e9d\u00e9ieren.HomeKit Bridges sinn op 150 Accessoire limit\u00e9iert, mat der Bridge selwer. Falls d'Bridge m\u00e9i w\u00e9i d\u00e9i max. Unzuel vun Accessoire soll \u00ebnnerst\u00ebtzen ass et recommand\u00e9iert verschidden HomeKit Bridges fir verschidden Domaine anzesetzen. Eng detaill\u00e9iert Konfiguratioun ass n\u00ebmme via YAML fir d\u00e9i prim\u00e4r Bridge verf\u00fcgbar.", + "title": "HomeKit Bridge aktiv\u00e9ieren" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)", + "safe_mode": "Safe Mode (n\u00ebmmen aktiv\u00e9ieren wann Kopplung net geht)" + }, + "title": "Erweidert Konfiguratioun" + }, + "cameras": { + "data": { + "camera_copy": "Kamera mat nativer H.264 Stream \u00cbnnerst\u00ebtzung" + }, + "description": "Iwwerpr\u00e9if all Kamera op nativ H.264 \u00cbnnerst\u00ebtzung. Falls d'Kamera keng H.264 Stream Ausgab huet, transkod\u00e9iert de System de Video op H.264 fir Homekit. Transkod\u00e9iere ben\u00e9idegt eng performant CPU an w\u00e4ert net anst\u00e4nneg op Single Board Computer funktion\u00e9ieren.", + "title": "Kamera Video Codec auswielen." + }, + "exclude": { + "data": { + "exclude_entities": "Entit\u00e9iten d\u00e9i ausgeschloss solle ginn" + } + }, + "init": { + "data": { + "include_domains": "Domaine d\u00e9i solle ageschloss ginn" + }, + "title": "Domaine auswielen fir an d'Bridge" + }, + "yaml": { + "description": "D\u00ebs Entr\u00e9e ass via YAML kontroll\u00e9iert", + "title": "HomeKit Bridge Optioune ajust\u00e9ieren" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json new file mode 100644 index 00000000000..538aa000172 --- /dev/null +++ b/homeassistant/components/homekit/translations/nl.json @@ -0,0 +1,21 @@ +{ + "options": { + "step": { + "advanced": { + "title": "Geavanceerde configuratie" + }, + "exclude": { + "data": { + "exclude_entities": "Uit te sluiten entiteiten" + }, + "description": "Kies de entiteiten die u NIET wilt overbruggen.", + "title": "Entiteiten in geselecteerde domeinen uitsluiten van bridge" + }, + "init": { + "data": { + "include_domains": "Op te nemen domeinen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json new file mode 100644 index 00000000000..836defe3b35 --- /dev/null +++ b/homeassistant/components/homekit/translations/no.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "port_name_in_use": "En bro med samme navn eller port er allerede konfigurert." + }, + "step": { + "pairing": { + "description": "S\u00e5 snart {name} bridge er klar, vil sammenkoblingen v\u00e6re tilgjengelig i \u201cVarsler\u201d som \u201cHomeKit Bridge Setup\u201d.", + "title": "Par HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", + "include_domains": "Domener \u00e5 inkludere" + }, + "description": "En HomeKit Bridge gir deg tilgang til home assistant-enhetene dine i HomeKit. HomeKit Bridges er begrenset til 150 tilbeh\u00f8r per forekomst, inkludert selve broen. Hvis du \u00f8nsker \u00e5 bygge bro over mer enn maksimalt antall tilbeh\u00f8r, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert enhetskonfigurasjon er bare tilgjengelig via YAML for hovedbroen.", + "title": "Aktiver HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)", + "zeroconf_default_interface": "Bruk standard zeroconf-grensesnitt (aktiver hvis broen ikke kan finnes i Hjem-appen)" + }, + "description": "Disse innstillingene m\u00e5 bare justeres dersom HomeKit bridge er ikke i bruk.", + "title": "Avansert konfigurasjon" + }, + "cameras": { + "data": { + "camera_copy": "Kameraer som st\u00f8tter opprinnelige H.264-str\u00f8mmer" + }, + "description": "Sjekk alle kameraer som st\u00f8tter opprinnelige H.264-str\u00f8mmer. Hvis kameraet ikke sender ut en H.264-str\u00f8m, vil systemet omkode videoen til H.264 for HomeKit. Transkoding krever en potent prosessor og er usannsynlig \u00e5 fungere p\u00e5 enkeltkortdatamaskiner som Raspberry Pi o.l.", + "title": "Velg videokodek for kamera." + }, + "exclude": { + "data": { + "exclude_entities": "Enheter \u00e5 ekskludere" + }, + "description": "Velg enhetene du IKKE vil skal bygges inn.", + "title": "Ekskluder enheter i utvalgte domener fra bridge" + }, + "init": { + "data": { + "include_domains": "Domener \u00e5 inkludere" + }, + "description": "Enheter i \"Domener som skal inkluderes\" vil bli brolagt til HomeKit. Du kan velge hvilke enheter som skal utelates fra denne listen p\u00e5 neste skjermbilde.", + "title": "Velg domener du vil bygge bro." + }, + "yaml": { + "description": "Denne oppf\u00f8ringen kontrolleres via YAML", + "title": "Juster alternativene for HomeKit Bridge" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json new file mode 100644 index 00000000000..3a3dfc22448 --- /dev/null +++ b/homeassistant/components/homekit/translations/pl.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Mostek o okre\u015blonej nazwie lub adresie IP jest ju\u017c skonfigurowany." + }, + "step": { + "pairing": { + "title": "Sparuj z mostkiem HomeKit" + }, + "user": { + "data": { + "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", + "include_domains": "Domeny do uwzgl\u0119dnienia" + }, + "title": "Aktywowanie mostka HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", + "safe_mode": "Tryb awaryjny (w\u0142\u0105cz tylko wtedy, gdy parowanie nie powiedzie si\u0119)" + }, + "title": "Konfiguracja zaawansowana" + }, + "cameras": { + "data": { + "camera_copy": "Kamery obs\u0142uguj\u0105ce kodek H.264" + }, + "description": "Sprawd\u017a, czy wszystkie kamery obs\u0142uguj\u0105 kodek H.264. Je\u015bli kamera nie wysy\u0142a strumienia skompresowanego kodekiem H.264, system b\u0119dzie transkodowa\u0142 wideo do H.264 dla HomeKit. Transkodowanie wymaga wydajnego procesora i jest ma\u0142o prawdopodobne, aby dzia\u0142a\u0142o na komputerach jednop\u0142ytkowych.", + "title": "Wyb\u00f3r kodeka wideo kamery" + }, + "exclude": { + "data": { + "exclude_entities": "Wykluczone encje" + }, + "title": "Wykluczenie encji z wybranych domen" + }, + "init": { + "data": { + "include_domains": "Wykluczone domeny" + }, + "description": "Encje w \"uwzgl\u0119dnionych domenach\" zostan\u0105 po\u0142\u0105czone z HomeKit. B\u0119dziesz m\u00f3g\u0142 wybra\u0107, kt\u00f3re encje maj\u0105 zosta\u0107 wykluczone z tej listy na nast\u0119pnym ekranie.", + "title": "Domeny do uwzgl\u0119dnienia." + }, + "yaml": { + "description": "Ten wpis jest kontrolowany przez YAML", + "title": "Dostosowywanie opcji mostka HomeKit" + } + } + }, + "title": "Mostek HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json new file mode 100644 index 00000000000..efec33bd187 --- /dev/null +++ b/homeassistant/components/homekit/translations/ru.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "pairing": { + "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0440\u0438\u0434\u0436 {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" + }, + "description": "HomeKit Bridge \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0432\u0430\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0432 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u043c\u043e\u0441\u0442. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.", + "title": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", + "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)", + "zeroconf_default_interface": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c zeroconf \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0431\u0440\u0438\u0434\u0436 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 '\u0414\u043e\u043c')." + }, + "description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit Bridge \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", + "title": "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + }, + "cameras": { + "data": { + "camera_copy": "\u041a\u0430\u043c\u0435\u0440\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043f\u043e\u0442\u043e\u043a\u0438 H.264" + }, + "description": "\u0415\u0441\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u0430 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u0438\u0442 \u043f\u043e\u0442\u043e\u043a H.264, \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0435\u0440\u0435\u043a\u043e\u0434\u0438\u0440\u0443\u0435\u0442 \u0432\u0438\u0434\u0435\u043e \u0432 H.264 \u0434\u043b\u044f HomeKit. \u0422\u0440\u0430\u043d\u0441\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0432\u044b\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u0430 \u0438 \u0432\u0440\u044f\u0434 \u043b\u0438 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0430 \u043e\u0434\u043d\u043e\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430\u0445.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0438\u0434\u0435\u043e\u043a\u043e\u0434\u0435\u043a \u043a\u0430\u043c\u0435\u0440\u044b." + }, + "exclude": { + "data": { + "exclude_entities": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u041d\u0415 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432 HomeKit.", + "title": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u0430\u0445" + }, + "init": { + "data": { + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" + }, + "description": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u0434\u043e\u043c\u0435\u043d\u0430\u043c, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432 HomeKit. \u041d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u044d\u0442\u0430\u043f\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0412\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u0431\u0440\u0430\u0442\u044c, \u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u0437 \u044d\u0442\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" + }, + "yaml": { + "description": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 YAML", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 HomeKit Bridge" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json new file mode 100644 index 00000000000..bfc315824ee --- /dev/null +++ b/homeassistant/components/homekit/translations/sl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Most z istim imenom ali vrati je \u017ee konfiguriran." + }, + "step": { + "pairing": { + "description": "Takoj, ko je most {name} pripravljen, bo zdru\u017eevanje na voljo v \"Obvestilih\" kot \"Nastavitev HomeKit mostu\".", + "title": "Upari HomeKit Most" + }, + "user": { + "data": { + "auto_start": "Samodejni zagon (onemogo\u010di, \u010de uporabljate Z-Wave ali drug sistem z zakasnjenim zagonom)", + "include_domains": "Domene, ki jih \u017eelite vklju\u010diti" + }, + "description": "HomeKit most vam bo omogo\u010dil dostop do Home Assistant entitet v HomeKit-u. HomeKit mostovi so omejeni na 150 entitet na primerek, vklju\u010dno z mostom. \u010ce \u017eelite premostiti dovoljeno \u0161tevilo dodatkov, priporo\u010damo, da uporabite ve\u010d mostov HomeKit za razli\u010dne domene. Podrobna konfiguracija entitete je na voljo samo prek YAML za primarni most.", + "title": "Aktivirajte HomeKit most" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Samodejni zagon (onemogo\u010dite, \u010de uporabljate Z-wave ali kakteri drug sistem z zakasnjenim zagonom)", + "safe_mode": "Varni na\u010din (omogo\u010dite samo, \u010de seznanjanje ne uspe)", + "zeroconf_default_interface": "Uporabite privzeti zeroconf vmesnik (omogo\u010dite, \u010de mostu ni mogo\u010de najti v Home Assistant-u)" + }, + "description": "Te nastavitve je treba prilagoditi le, \u010de most HomeKit ni funkcionalen.", + "title": "Napredna konfiguracija" + }, + "exclude": { + "data": { + "exclude_entities": "Subjekti, ki jih je treba izklju\u010diti" + }, + "description": "Izberite entitete, ki jih NE \u017eelite premostiti.", + "title": "Iz mostu izklju\u010dite subjekte izbranih domen" + }, + "init": { + "data": { + "include_domains": "Domene za vklju\u010ditev" + }, + "description": "Subjekti v domenah, ki jih \u017eelite vklju\u010diti, bodo prevezani na HomeKit. Na naslednjem zaslonu boste lahko izbrali subjekte, ki jih \u017eelite izklju\u010diti s tega seznama.", + "title": "Izberite domene za premostitev." + }, + "yaml": { + "description": "Ta vnos je pod nadzorom YAML", + "title": "Prilagodi mo\u017enosti mosta HomeKit." + } + } + }, + "title": "HomeKit Most" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json new file mode 100644 index 00000000000..7b03ac36f3b --- /dev/null +++ b/homeassistant/components/homekit/translations/sv.json @@ -0,0 +1,14 @@ +{ + "options": { + "step": { + "cameras": { + "data": { + "camera_copy": "Kameror som st\u00f6der inbyggda H.264-str\u00f6mmar" + }, + "description": "Kontrollera alla kameror som st\u00f6der inbyggda H.264-str\u00f6mmar. Om kameran inte skickar ut en H.264-str\u00f6m kodar systemet videon till H.264 f\u00f6r HomeKit. Transkodning kr\u00e4ver h\u00f6g prestanda och kommer troligtvis inte att fungera p\u00e5 enkortsdatorer.", + "title": "V\u00e4lj kamerans videoavkodare." + } + } + }, + "title": "HomeKit-brygga" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json new file mode 100644 index 00000000000..c47a6fe104f --- /dev/null +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\u5df2\u914d\u7f6e\u8fc7\u5177\u6709\u76f8\u540c\u540d\u79f0\u6216\u7aef\u53e3\u7684\u6865\u63a5\u5668\u3002" + }, + "step": { + "pairing": { + "description": "\u4e00\u65e6\u6865\u63a5\u5668 {name} \u51c6\u5907\u5c31\u7eea\uff0c\u5c31\u53ef\u4ee5\u5728\u201c\u901a\u77e5\u201d\u627e\u5230\u201cHomeKit \u6865\u63a5\u5668\u914d\u7f6e\u201d\u8fdb\u884c\u914d\u5bf9\u3002", + "title": "\u914d\u5bf9 HomeKit \u6865\u63a5\u5668" + }, + "user": { + "data": { + "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", + "include_domains": "\u8981\u5305\u542b\u7684\u57df" + }, + "description": "HomeKit \u6865\u63a5\u5668\u53ef\u4ee5\u8ba9\u60a8\u901a\u8fc7 HomeKit \u8bbf\u95ee Home Assistant \u4e2d\u7684\u5b9e\u4f53\u3002\u6bcf\u4e2a\u6865\u63a5\u5668\u5b9e\u4f8b\u6700\u591a\u53ef\u6a21\u62df 150 \u4e2a\u914d\u4ef6\uff0c\u5305\u62ec\u6865\u63a5\u5668\u672c\u8eab\u3002\u5982\u679c\u60a8\u5e0c\u671b\u6865\u63a5\u7684\u914d\u4ef6\u591a\u4e8e\u6b64\u6570\u91cf\uff0c\u5efa\u8bae\u4e3a\u4e0d\u540c\u7684\u57df\u4f7f\u7528\u591a\u4e2a HomeKit \u6865\u63a5\u5668\u3002\u8be6\u7ec6\u7684\u5b9e\u4f53\u914d\u7f6e\u4ec5\u53ef\u7528\u4e8e\u4e3b\u6865\u63a5\u5668\uff0c\u4e14\u987b\u901a\u8fc7 YAML \u914d\u7f6e\u3002", + "title": "\u6fc0\u6d3b HomeKit \u6865\u63a5\u5668" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", + "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u4ec5\u5728\u914d\u5bf9\u5931\u8d25\u65f6\u542f\u7528\uff09", + "zeroconf_default_interface": "\u4f7f\u7528\u9ed8\u8ba4\u7684 zeroconf \u63a5\u53e3\uff08\u5982\u679c\u5728\u201c\u5bb6\u5ead\u201d\u5e94\u7528\u7a0b\u5e8f\u4e2d\u627e\u4e0d\u5230\u6865\u63a5\u5668\u5219\u542f\u7528\uff09" + }, + "description": "\u8fd9\u4e9b\u8bbe\u7f6e\u53ea\u6709\u5f53 HomeKit \u6865\u63a5\u5668\u529f\u80fd\u4e0d\u6b63\u5e38\u65f6\u624d\u9700\u8981\u8c03\u6574\u3002", + "title": "\u9ad8\u7ea7\u914d\u7f6e" + }, + "exclude": { + "data": { + "exclude_entities": "\u8981\u6392\u9664\u7684\u5b9e\u4f53" + }, + "description": "\u9009\u62e9\u4e0d\u9700\u8981\u6865\u63a5\u7684\u5b9e\u4f53\u3002", + "title": "\u5bf9\u9009\u62e9\u7684\u57df\u6392\u9664\u5b9e\u4f53" + }, + "init": { + "data": { + "include_domains": "\u8981\u5305\u542b\u7684\u57df" + }, + "description": "\u201c\u8981\u5305\u542b\u7684\u57df\u201d\u4e2d\u7684\u5b9e\u4f53\u5c06\u88ab\u6865\u63a5\u5230 HomeKit\u3002\u5728\u4e0b\u4e00\u9875\u53ef\u4ee5\u9009\u62e9\u8981\u6392\u9664\u5176\u4e2d\u7684\u54ea\u4e9b\u5b9e\u4f53\u3002", + "title": "\u9009\u62e9\u8981\u6865\u63a5\u7684\u57df\u3002" + } + } + }, + "title": "HomeKit \u6865\u63a5\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json new file mode 100644 index 00000000000..166fefb9843 --- /dev/null +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\u4f7f\u7528\u76f8\u540c\u540d\u7a31\u6216\u901a\u8a0a\u57e0\u7684 Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "pairing": { + "description": "\u65bc {name} bridge \u5c31\u7dd2\u5f8c\u3001\u5c07\u6703\u65bc\u300c\u901a\u77e5\u300d\u4e2d\u986f\u793a\u300cHomeKit Bridge \u8a2d\u5b9a\u300d\u7684\u914d\u5c0d\u8cc7\u8a0a\u3002", + "title": "\u914d\u5c0d HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", + "include_domains": "\u5305\u542b Domain" + }, + "description": "HomeKit Bridge \u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u7269\u4ef6\u3002HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u7269\u4ef6\u3002", + "title": "\u555f\u7528 HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", + "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09", + "zeroconf_default_interface": "\u4f7f\u7528\u9810\u8a2d zeroconf \u4ecb\u9762\uff08\u50c5\u65bc\u5bb6\u5ead App \u627e\u4e0d\u5230 Bridge \u6642\u958b\u555f\uff09" + }, + "description": "\u50c5\u65bc Homekit bridge \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002", + "title": "\u9032\u968e\u8a2d\u5b9a" + }, + "cameras": { + "data": { + "camera_copy": "\u652f\u63f4\u539f\u751f H.264 \u4e32\u6d41\u651d\u5f71\u6a5f" + }, + "description": "\u6aa2\u67e5\u6240\u6709\u652f\u63f4\u539f\u751f H.264 \u4e32\u6d41\u4e4b\u651d\u5f71\u6a5f\u3002\u5047\u5982\u651d\u5f71\u6a5f\u4e0d\u652f\u63f4 H.264 \u4e32\u6d41\u3001\u7cfb\u7d71\u5c07\u6703\u91dd\u5c0d Homekit \u9032\u884c H.264 \u8f49\u78bc\u3002\u8f49\u78bc\u5c07\u9700\u8981\u4f7f\u7528 CPU \u9032\u884c\u904b\u7b97\u3001\u55ae\u6676\u7247\u96fb\u8166\u53ef\u80fd\u6703\u906d\u9047\u6548\u80fd\u554f\u984c\u3002", + "title": "\u9078\u64c7\u651d\u5f71\u6a5f\u7de8\u78bc\u3002" + }, + "exclude": { + "data": { + "exclude_entities": "\u6392\u9664\u7269\u4ef6" + }, + "description": "\u9078\u64c7\u4e0d\u9032\u884c\u6a4b\u63a5\u7684\u7269\u4ef6\u3002", + "title": "\u65bc\u6240\u9078 Domain \u4e2d\u6240\u8981\u6392\u9664\u7684\u7269\u4ef6" + }, + "init": { + "data": { + "include_domains": "\u5305\u542b Domain" + }, + "description": "\u300c\u5305\u542b Domain\u300d\u4e2d\u7684\u7269\u4ef6\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u7269\u4ef6\u5217\u8868\u3002", + "title": "\u9078\u64c7\u6240\u8981\u6a4b\u63a5\u7684 Domain\u3002" + }, + "yaml": { + "description": "\u6b64\u7269\u4ef6\u70ba\u900f\u904e YAML \u63a7\u5236", + "title": "\u8abf\u6574 HomeKit Bridge \u9078\u9805" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py new file mode 100644 index 00000000000..c4e52f07832 --- /dev/null +++ b/homeassistant/components/homekit/type_cameras.py @@ -0,0 +1,341 @@ +"""Class to hold all camera accessories.""" +import asyncio +from datetime import timedelta +import logging + +from haffmpeg.core import HAFFmpeg +from pyhap.camera import ( + STREAMING_STATUS, + VIDEO_CODEC_PARAM_LEVEL_TYPES, + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES, + Camera as PyhapCamera, +) +from pyhap.const import CATEGORY_CAMERA + +from homeassistant.components.camera.const import DOMAIN as DOMAIN_CAMERA +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import get_local_ip + +from .accessories import TYPES, HomeAccessory +from .const import ( + CHAR_STREAMING_STRATUS, + CONF_AUDIO_CODEC, + CONF_AUDIO_MAP, + CONF_AUDIO_PACKET_SIZE, + CONF_MAX_FPS, + CONF_MAX_HEIGHT, + CONF_MAX_WIDTH, + CONF_STREAM_ADDRESS, + CONF_STREAM_SOURCE, + CONF_SUPPORT_AUDIO, + CONF_VIDEO_CODEC, + CONF_VIDEO_MAP, + CONF_VIDEO_PACKET_SIZE, + DEFAULT_AUDIO_CODEC, + DEFAULT_AUDIO_MAP, + DEFAULT_AUDIO_PACKET_SIZE, + DEFAULT_MAX_FPS, + DEFAULT_MAX_HEIGHT, + DEFAULT_MAX_WIDTH, + DEFAULT_SUPPORT_AUDIO, + DEFAULT_VIDEO_CODEC, + DEFAULT_VIDEO_MAP, + DEFAULT_VIDEO_PACKET_SIZE, + SERV_CAMERA_RTP_STREAM_MANAGEMENT, +) +from .img_util import scale_jpeg_camera_image +from .util import pid_is_alive + +_LOGGER = logging.getLogger(__name__) + + +VIDEO_OUTPUT = ( + "-map {v_map} -an " + "-c:v {v_codec} " + "{v_profile}" + "-tune zerolatency -pix_fmt yuv420p " + "-r {fps} " + "-b:v {v_max_bitrate}k -bufsize {v_bufsize}k -maxrate {v_max_bitrate}k " + "-payload_type 99 " + "-ssrc {v_ssrc} -f rtp " + "-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params {v_srtp_key} " + "srtp://{address}:{v_port}?rtcpport={v_port}&" + "localrtcpport={v_port}&pkt_size={v_pkt_size}" +) + +AUDIO_OUTPUT = ( + "-map {a_map} -vn " + "-c:a {a_encoder} " + "{a_application}" + "-ac 1 -ar {a_sample_rate}k " + "-b:a {a_max_bitrate}k -bufsize {a_bufsize}k " + "-payload_type 110 " + "-ssrc {a_ssrc} -f rtp " + "-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params {a_srtp_key} " + "srtp://{address}:{a_port}?rtcpport={a_port}&" + "localrtcpport={a_port}&pkt_size={a_pkt_size}" +) + +SLOW_RESOLUTIONS = [ + (320, 180, 15), + (320, 240, 15), +] + +RESOLUTIONS = [ + (320, 180), + (320, 240), + (480, 270), + (480, 360), + (640, 360), + (640, 480), + (1024, 576), + (1024, 768), + (1280, 720), + (1280, 960), + (1920, 1080), +] + +VIDEO_PROFILE_NAMES = ["baseline", "main", "high"] + +FFMPEG_WATCH_INTERVAL = timedelta(seconds=5) +FFMPEG_WATCHER = "ffmpeg_watcher" +FFMPEG_PID = "ffmpeg_pid" +SESSION_ID = "session_id" + +CONFIG_DEFAULTS = { + CONF_SUPPORT_AUDIO: DEFAULT_SUPPORT_AUDIO, + CONF_MAX_WIDTH: DEFAULT_MAX_WIDTH, + CONF_MAX_HEIGHT: DEFAULT_MAX_HEIGHT, + CONF_MAX_FPS: DEFAULT_MAX_FPS, + CONF_AUDIO_CODEC: DEFAULT_AUDIO_CODEC, + CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP, + CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP, + CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC, + CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE, + CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE, +} + + +@TYPES.register("Camera") +class Camera(HomeAccessory, PyhapCamera): + """Generate a Camera accessory.""" + + def __init__(self, hass, driver, name, entity_id, aid, config): + """Initialize a Camera accessory object.""" + self._ffmpeg = hass.data[DATA_FFMPEG] + self._cur_session = None + self._camera = hass.data[DOMAIN_CAMERA] + for config_key in CONFIG_DEFAULTS: + if config_key not in config: + config[config_key] = CONFIG_DEFAULTS[config_key] + + max_fps = config[CONF_MAX_FPS] + max_width = config[CONF_MAX_WIDTH] + max_height = config[CONF_MAX_HEIGHT] + resolutions = [ + (w, h, fps) + for w, h, fps in SLOW_RESOLUTIONS + if w <= max_width and h <= max_height and fps < max_fps + ] + [ + (w, h, max_fps) + for w, h in RESOLUTIONS + if w <= max_width and h <= max_height + ] + + video_options = { + "codec": { + "profiles": [ + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES["BASELINE"], + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES["MAIN"], + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES["HIGH"], + ], + "levels": [ + VIDEO_CODEC_PARAM_LEVEL_TYPES["TYPE3_1"], + VIDEO_CODEC_PARAM_LEVEL_TYPES["TYPE3_2"], + VIDEO_CODEC_PARAM_LEVEL_TYPES["TYPE4_0"], + ], + }, + "resolutions": resolutions, + } + audio_options = {"codecs": [{"type": "OPUS", "samplerate": 24}]} + + stream_address = config.get(CONF_STREAM_ADDRESS, get_local_ip()) + + options = { + "video": video_options, + "audio": audio_options, + "address": stream_address, + "srtp": True, + } + + super().__init__( + hass, + driver, + name, + entity_id, + aid, + config, + category=CATEGORY_CAMERA, + options=options, + ) + + @callback + def async_update_state(self, new_state): + """Handle state change to update HomeKit value.""" + pass # pylint: disable=unnecessary-pass + + async def _async_get_stream_source(self): + """Find the camera stream source url.""" + camera = self._camera.get_entity(self.entity_id) + if not camera or not camera.is_on: + return None + stream_source = self.config.get(CONF_STREAM_SOURCE) + if stream_source: + return stream_source + try: + stream_source = await camera.stream_source() + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Failed to get stream source - this could be a transient error or your camera might not be compatible with HomeKit yet" + ) + if stream_source: + self.config[CONF_STREAM_SOURCE] = stream_source + return stream_source + + async def start_stream(self, session_info, stream_config): + """Start a new stream with the given configuration.""" + _LOGGER.debug( + "[%s] Starting stream with the following parameters: %s", + session_info["id"], + stream_config, + ) + input_source = await self._async_get_stream_source() + if not input_source: + _LOGGER.error("Camera has no stream source") + return False + if "-i " not in input_source: + input_source = "-i " + input_source + video_profile = "" + if self.config[CONF_VIDEO_CODEC] != "copy": + video_profile = ( + "-profile:v " + + VIDEO_PROFILE_NAMES[ + int.from_bytes(stream_config["v_profile_id"], byteorder="big") + ] + + " " + ) + audio_application = "" + if self.config[CONF_AUDIO_CODEC] == "libopus": + audio_application = "-application lowdelay " + output_vars = stream_config.copy() + output_vars.update( + { + "v_profile": video_profile, + "v_bufsize": stream_config["v_max_bitrate"] * 4, + "v_map": self.config[CONF_VIDEO_MAP], + "v_pkt_size": self.config[CONF_VIDEO_PACKET_SIZE], + "v_codec": self.config[CONF_VIDEO_CODEC], + "a_bufsize": stream_config["a_max_bitrate"] * 4, + "a_map": self.config[CONF_AUDIO_MAP], + "a_pkt_size": self.config[CONF_AUDIO_PACKET_SIZE], + "a_encoder": self.config[CONF_AUDIO_CODEC], + "a_application": audio_application, + } + ) + output = VIDEO_OUTPUT.format(**output_vars) + if self.config[CONF_SUPPORT_AUDIO]: + output = output + " " + AUDIO_OUTPUT.format(**output_vars) + _LOGGER.debug("FFmpeg output settings: %s", output) + stream = HAFFmpeg(self._ffmpeg.binary, loop=self.driver.loop) + opened = await stream.open( + cmd=[], input_source=input_source, output=output, stdout_pipe=False + ) + if not opened: + _LOGGER.error("Failed to open ffmpeg stream") + return False + session_info["stream"] = stream + _LOGGER.info( + "[%s] Started stream process - PID %d", + session_info["id"], + stream.process.pid, + ) + + ffmpeg_watcher = async_track_time_interval( + self.hass, self._async_ffmpeg_watch, FFMPEG_WATCH_INTERVAL + ) + self._cur_session = { + FFMPEG_WATCHER: ffmpeg_watcher, + FFMPEG_PID: stream.process.pid, + SESSION_ID: session_info["id"], + } + + return await self._async_ffmpeg_watch(0) + + async def _async_ffmpeg_watch(self, _): + """Check to make sure ffmpeg is still running and cleanup if not.""" + ffmpeg_pid = self._cur_session[FFMPEG_PID] + session_id = self._cur_session[SESSION_ID] + if pid_is_alive(ffmpeg_pid): + return True + + _LOGGER.warning("Streaming process ended unexpectedly - PID %d", ffmpeg_pid) + self._async_stop_ffmpeg_watch() + self._async_set_streaming_available(session_id) + return False + + @callback + def _async_stop_ffmpeg_watch(self): + """Cleanup a streaming session after stopping.""" + if not self._cur_session: + return + self._cur_session[FFMPEG_WATCHER]() + self._cur_session = None + + @callback + def _async_set_streaming_available(self, session_id): + """Free the session so they can start another.""" + self.streaming_status = STREAMING_STATUS["AVAILABLE"] + self.get_service(SERV_CAMERA_RTP_STREAM_MANAGEMENT).get_characteristic( + CHAR_STREAMING_STRATUS + ).notify() + + async def stop_stream(self, session_info): + """Stop the stream for the given ``session_id``.""" + session_id = session_info["id"] + stream = session_info.get("stream") + if not stream: + _LOGGER.debug("No stream for session ID %s", session_id) + return + + self._async_stop_ffmpeg_watch() + + if not pid_is_alive(stream.process.pid): + _LOGGER.info("[%s] Stream already stopped.", session_id) + return True + + for shutdown_method in ["close", "kill"]: + _LOGGER.info("[%s] %s stream.", session_id, shutdown_method) + try: + await getattr(stream, shutdown_method)() + return + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "[%s] Failed to %s stream.", session_id, shutdown_method + ) + + async def reconfigure_stream(self, session_info, stream_config): + """Reconfigure the stream so that it uses the given ``stream_config``.""" + return True + + def get_snapshot(self, image_size): + """Return a jpeg of a snapshot from the camera.""" + return scale_jpeg_camera_image( + asyncio.run_coroutine_threadsafe( + self.hass.components.camera.async_get_image(self.entity_id), + self.hass.loop, + ).result(), + image_size["image-width"], + image_size["image-height"], + ) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 987ba900bc8..1e18ad82b94 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -25,9 +25,9 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import TYPES, HomeAccessory, debounce from .const import ( CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, @@ -93,7 +93,7 @@ class GarageDoorOpener(HomeAccessory): self.char_target_state = serv_garage_door.configure_char( CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state ) - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Change garage state if call came from HomeKit.""" @@ -109,7 +109,8 @@ class GarageDoorOpener(HomeAccessory): self.char_current_state.set_value(HK_DOOR_CLOSING) self.call_service(DOMAIN, SERVICE_CLOSE_COVER, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover state after state changed.""" hass_state = new_state.state target_door_state = DOOR_TARGET_HASS_TO_HK.get(hass_state) @@ -185,7 +186,8 @@ class WindowCoveringBase(HomeAccessory): self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover position and tilt after state changed.""" # update tilt current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION) @@ -231,7 +233,7 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): self.char_position_state = self.serv_cover.configure_char( CHAR_POSITION_STATE, value=HK_POSITION_STOPPED ) - self.update_state(state) + self.async_update_state(state) @debounce def move_cover(self, value): @@ -242,7 +244,8 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover position and tilt after state changed.""" current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, (float, int)): @@ -272,7 +275,7 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): if self.char_position_state.value != HK_POSITION_STOPPED: self.char_position_state.set_value(HK_POSITION_STOPPED) - super().update_state(new_state) + super().async_update_state(new_state) @TYPES.register("WindowCoveringBasic") @@ -296,7 +299,7 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): self.char_position_state = self.serv_cover.configure_char( CHAR_POSITION_STATE, value=HK_POSITION_STOPPED ) - self.update_state(state) + self.async_update_state(state) @debounce def move_cover(self, value): @@ -323,7 +326,8 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): self.char_current_position.set_value(position) self.char_target_position.set_value(position) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover position after state changed.""" position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0} hk_position = position_mapping.get(new_state.state) @@ -342,4 +346,4 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): if self.char_position_state.value != HK_POSITION_STOPPED: self.char_position_state.set_value(HK_POSITION_STOPPED) - super().update_state(new_state) + super().async_update_state(new_state) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 291b3ffed90..d6231efe7ad 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -26,9 +26,9 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, @@ -81,13 +81,13 @@ class Fan(HomeAccessory): if CHAR_ROTATION_SPEED in chars: # Initial value is set to 100 because 0 is a special value (off). 100 is - # an arbitrary non-zero value. It is updated immediately by update_state + # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_speed = serv_fan.configure_char(CHAR_ROTATION_SPEED, value=100) if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) - self.update_state(state) + self.async_update_state(state) serv_fan.setter_callback = self._set_chars def _set_chars(self, char_values): @@ -147,7 +147,8 @@ class Fan(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SPEED: speed} self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update fan after state change.""" # Handle State state = new_state.state diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8458c8351da..612d8e53a02 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -23,9 +23,9 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, @@ -78,7 +78,7 @@ class Light(HomeAccessory): if CHAR_BRIGHTNESS in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is - # an arbitrary non-zero value. It is updated immediately by update_state + # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) @@ -101,7 +101,7 @@ class Light(HomeAccessory): if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) - self.update_state(state) + self.async_update_state(state) serv_light.setter_callback = self._set_chars @@ -139,7 +139,8 @@ class Light(HomeAccessory): self.call_service(DOMAIN, service, params, ", ".join(events)) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update light after state change.""" # Handle State state = new_state.state diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 0d2a19ef089..af5b24c50e1 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -5,9 +5,9 @@ from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import DOMAIN, STATE_LOCKED, STATE_UNLOCKED from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) @@ -46,7 +46,7 @@ class Lock(HomeAccessory): value=HASS_TO_HOMEKIT[STATE_LOCKED], setter_callback=self.set_state, ) - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" @@ -63,7 +63,8 @@ class Lock(HomeAccessory): params[ATTR_CODE] = self._code self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update lock after state changed.""" hass_state = new_state.state if hass_state in HASS_TO_HOMEKIT: diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 76f4532b868..4a104972b02 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -36,9 +36,9 @@ from homeassistant.const import ( STATE_STANDBY, STATE_UNKNOWN, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, CHAR_ACTIVE_IDENTIFIER, @@ -94,6 +94,13 @@ MODE_FRIENDLY_NAME = { FEATURE_TOGGLE_MUTE: "Mute", } +MEDIA_PLAYER_OFF_STATES = ( + STATE_OFF, + STATE_UNKNOWN, + STATE_STANDBY, + "None", +) + @TYPES.register("MediaPlayer") class MediaPlayer(HomeAccessory): @@ -144,7 +151,7 @@ class MediaPlayer(HomeAccessory): self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char( CHAR_ON, value=False, setter_callback=self.set_toggle_mute ) - self.update_state(state) + self.async_update_state(state) def generate_service_name(self, mode): """Generate name for individual service.""" @@ -183,17 +190,13 @@ class MediaPlayer(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state if self.chars[FEATURE_ON_OFF]: - hk_state = current_state not in ( - STATE_OFF, - STATE_UNKNOWN, - STATE_STANDBY, - "None", - ) + hk_state = current_state not in MEDIA_PLAYER_OFF_STATES _LOGGER.debug( '%s: Set current state for "on_off" to %s', self.entity_id, hk_state ) @@ -320,7 +323,7 @@ class TelevisionMediaPlayer(HomeAccessory): serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) _LOGGER.debug("%s: Added source %s.", self.entity_id, source) - self.update_state(state) + self.async_update_state(state) def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" @@ -374,13 +377,14 @@ class TelevisionMediaPlayer(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update Television state after state changed.""" current_state = new_state.state # Power state television hk_state = 0 - if current_state not in ("None", STATE_OFF, STATE_UNKNOWN): + if current_state not in MEDIA_PLAYER_OFF_STATES: hk_state = 1 _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) if self.char_active.value != hk_state: diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 59e10a42c29..7d8dcac046d 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -17,9 +17,9 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, @@ -65,7 +65,7 @@ class SecuritySystem(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" @@ -78,7 +78,8 @@ class SecuritySystem(HomeAccessory): params[ATTR_CODE] = self._alarm_code self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update security state after state changed.""" hass_state = new_state.state if hass_state in HASS_TO_HOMEKIT: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 5bdf9a07f04..28c7ea26009 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -10,9 +10,9 @@ from homeassistant.const import ( STATE_ON, TEMP_CELSIUS, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, @@ -90,9 +90,10 @@ class TemperatureSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update temperature after state changed.""" unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) temperature = convert_to_float(new_state.state) @@ -119,9 +120,10 @@ class HumiditySensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" humidity = convert_to_float(new_state.state) if humidity and self.char_humidity.value != humidity: @@ -146,9 +148,10 @@ class AirQualitySensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) if density: @@ -182,9 +185,10 @@ class CarbonMonoxideSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: @@ -219,9 +223,10 @@ class CarbonDioxideSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: @@ -249,9 +254,10 @@ class LightSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) if luminance and self.char_light.value != luminance: @@ -282,9 +288,10 @@ class BinarySensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" state = new_state.state detected = self.format(state in (STATE_ON, STATE_HOME)) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index a1088f110e5..635b0e1d036 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -24,11 +24,10 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import split_entity_id +from homeassistant.core import callback, split_entity_id from homeassistant.helpers.event import call_later -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, CHAR_IN_USE, @@ -72,7 +71,7 @@ class Outlet(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" @@ -81,7 +80,8 @@ class Outlet(HomeAccessory): service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state == STATE_ON if self.char_on.value is not current_state: @@ -107,7 +107,7 @@ class Switch(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def is_activate(self, state): """Check if entity is activate only.""" @@ -137,7 +137,8 @@ class Switch(HomeAccessory): if self.activate_only: call_later(self.hass, 1, self.reset_switch) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" self.activate_only = self.is_activate(new_state) if self.activate_only: @@ -163,7 +164,8 @@ class DockVacuum(Switch): service = SERVICE_START if value else SERVICE_RETURN_TO_BASE self.call_service(VACUUM_DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state in (STATE_CLEANING, STATE_ON) if self.char_on.value is not current_state: @@ -192,7 +194,7 @@ class Valve(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Move value state to value if call came from HomeKit.""" @@ -202,7 +204,8 @@ class Valve(HomeAccessory): service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = 1 if new_state.state == STATE_ON else 0 if self.char_active.value != current_state: diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 0da92ef3dba..c202191cb7e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -51,9 +51,9 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, UNIT_PERCENTAGE, ) +from homeassistant.core import callback -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, @@ -221,7 +221,7 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_HUMIDITY, value=50 ) - self._update_state(state) + self._async_update_state(state) serv_thermostat.setter_callback = self._set_chars @@ -392,7 +392,8 @@ class Thermostat(HomeAccessory): DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{UNIT_PERCENTAGE}" ) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update thermostat state after state changed.""" if self._state_updates < 3: # When we get the first state updates @@ -415,9 +416,10 @@ class Thermostat(HomeAccessory): ) self._state_updates += 1 - self._update_state(new_state) + self._async_update_state(new_state) - def _update_state(self, new_state): + @callback + def _async_update_state(self, new_state): """Update state without rechecking the device features.""" features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -545,7 +547,7 @@ class WaterHeater(HomeAccessory): ) state = self.hass.states.get(self.entity_id) - self.update_state(state) + self.async_update_state(state) def get_temperature_range(self): """Return min and max temperature range.""" @@ -587,7 +589,8 @@ class WaterHeater(HomeAccessory): f"{temperature}{self._unit}", ) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update water_heater state after state change.""" # Update current and target temperature temperature = new_state.attributes.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 9c8e7e8c053..d35c463ca39 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,8 +1,12 @@ """Collection of useful functions for the HomeKit component.""" from collections import OrderedDict, namedtuple import io +import ipaddress import logging +import os +import re import secrets +import socket import pyqrcode import voluptuous as vol @@ -15,21 +19,47 @@ from homeassistant.const import ( CONF_TYPE, TEMP_CELSIUS, ) -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, split_entity_id import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.temperature as temp_util from .const import ( + AUDIO_CODEC_COPY, + AUDIO_CODEC_OPUS, + CONF_AUDIO_CODEC, + CONF_AUDIO_MAP, + CONF_AUDIO_PACKET_SIZE, CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + CONF_MAX_FPS, + CONF_MAX_HEIGHT, + CONF_MAX_WIDTH, + CONF_STREAM_ADDRESS, + CONF_STREAM_SOURCE, + CONF_SUPPORT_AUDIO, + CONF_VIDEO_CODEC, + CONF_VIDEO_MAP, + CONF_VIDEO_PACKET_SIZE, + DEFAULT_AUDIO_CODEC, + DEFAULT_AUDIO_MAP, + DEFAULT_AUDIO_PACKET_SIZE, DEFAULT_LOW_BATTERY_THRESHOLD, + DEFAULT_MAX_FPS, + DEFAULT_MAX_HEIGHT, + DEFAULT_MAX_WIDTH, + DEFAULT_SUPPORT_AUDIO, + DEFAULT_VIDEO_CODEC, + DEFAULT_VIDEO_MAP, + DEFAULT_VIDEO_PACKET_SIZE, + DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, - HOMEKIT_NOTIFY_ID, + HOMEKIT_FILE, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, TYPE_FAUCET, @@ -38,10 +68,16 @@ from .const import ( TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE, + VIDEO_CODEC_COPY, + VIDEO_CODEC_H264_OMX, + VIDEO_CODEC_LIBX264, ) _LOGGER = logging.getLogger(__name__) +MAX_PORT = 65535 +VALID_VIDEO_CODECS = [VIDEO_CODEC_LIBX264, VIDEO_CODEC_H264_OMX, AUDIO_CODEC_COPY] +VALID_AUDIO_CODECS = [AUDIO_CODEC_OPUS, VIDEO_CODEC_COPY] BASIC_INFO_SCHEMA = vol.Schema( { @@ -57,6 +93,31 @@ FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend( {vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list} ) +CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend( + { + vol.Optional(CONF_STREAM_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_STREAM_SOURCE): cv.string, + vol.Optional(CONF_AUDIO_CODEC, default=DEFAULT_AUDIO_CODEC): vol.In( + VALID_AUDIO_CODECS + ), + vol.Optional(CONF_SUPPORT_AUDIO, default=DEFAULT_SUPPORT_AUDIO): cv.boolean, + vol.Optional(CONF_MAX_WIDTH, default=DEFAULT_MAX_WIDTH): cv.positive_int, + vol.Optional(CONF_MAX_HEIGHT, default=DEFAULT_MAX_HEIGHT): cv.positive_int, + vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int, + vol.Optional(CONF_AUDIO_MAP, default=DEFAULT_AUDIO_MAP): cv.string, + vol.Optional(CONF_VIDEO_MAP, default=DEFAULT_VIDEO_MAP): cv.string, + vol.Optional(CONF_VIDEO_CODEC, default=DEFAULT_VIDEO_CODEC): vol.In( + VALID_VIDEO_CODECS + ), + vol.Optional( + CONF_AUDIO_PACKET_SIZE, default=DEFAULT_AUDIO_PACKET_SIZE + ): cv.positive_int, + vol.Optional( + CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE + ): cv.positive_int, + } +) + CODE_SCHEMA = BASIC_INFO_SCHEMA.extend( {vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)} ) @@ -157,6 +218,9 @@ def validate_entity_config(values): feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list + elif domain == "camera": + config = CAMERA_SCHEMA(config) + elif domain == "switch": config = SWITCH_TYPE_SCHEMA(config) @@ -260,7 +324,7 @@ class HomeKitSpeedMapping: return list(self.speed_ranges.keys())[0] -def show_setup_message(hass, pincode, uri): +def show_setup_message(hass, entry_id, bridge_name, pincode, uri): """Display persistent notification with setup information.""" pin = pincode.decode() _LOGGER.info("Pincode: %s", pin) @@ -270,23 +334,23 @@ def show_setup_message(hass, pincode, uri): url.svg(buffer, scale=5) pairing_secret = secrets.token_hex(32) - hass.data[HOMEKIT_PAIRING_QR] = buffer.getvalue() - hass.data[HOMEKIT_PAIRING_QR_SECRET] = pairing_secret + hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR] = buffer.getvalue() + hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR_SECRET] = pairing_secret message = ( - f"To set up Home Assistant in the Home App, " + f"To set up {bridge_name} in the Home App, " f"scan the QR code or enter the following code:\n" f"### {pin}\n" - f"![image](/api/homekit/pairingqr?{pairing_secret})" + f"![image](/api/homekit/pairingqr?{entry_id}-{pairing_secret})" ) hass.components.persistent_notification.create( - message, "HomeKit Setup", HOMEKIT_NOTIFY_ID + message, "HomeKit Bridge Setup", entry_id ) -def dismiss_setup_message(hass): +def dismiss_setup_message(hass, entry_id): """Dismiss persistent notification and remove QR code.""" - hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + hass.components.persistent_notification.dismiss(entry_id) def convert_to_float(state): @@ -328,3 +392,103 @@ def density_to_air_quality(density): if density <= 150: return 4 return 5 + + +def get_persist_filename_for_entry_id(entry_id: str): + """Determine the filename of the homekit state file.""" + return f"{DOMAIN}.{entry_id}.state" + + +def get_aid_storage_filename_for_entry_id(entry_id: str): + """Determine the ilename of homekit aid storage file.""" + return f"{DOMAIN}.{entry_id}.aids" + + +def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): + """Determine the path to the homekit state file.""" + return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id)) + + +def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): + """Determine the path to the homekit aid storage file.""" + return hass.config.path( + STORAGE_DIR, get_aid_storage_filename_for_entry_id(entry_id) + ) + + +def format_sw_version(version): + """Extract the version string in a format homekit can consume.""" + match = re.search(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?", str(version).replace("-", ".")) + if match: + return match.group(0) + return None + + +def migrate_filesystem_state_data_for_primary_imported_entry_id( + hass: HomeAssistant, entry_id: str +): + """Migrate the old paths to the storage directory.""" + legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) + if os.path.exists(legacy_persist_file_path): + os.rename( + legacy_persist_file_path, get_persist_fullpath_for_entry_id(hass, entry_id) + ) + + legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") + if os.path.exists(legacy_aid_storage_path): + os.rename( + legacy_aid_storage_path, + get_aid_storage_fullpath_for_entry_id(hass, entry_id), + ) + + +def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): + """Remove the state files from disk.""" + persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id) + aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id) + os.unlink(persist_file_path) + if os.path.exists(aid_storage_path): + os.unlink(aid_storage_path) + return True + + +def _get_test_socket(): + """Create a socket to test binding ports.""" + test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + test_socket.setblocking(False) + test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return test_socket + + +def port_is_available(port: int): + """Check to see if a port is available.""" + test_socket = _get_test_socket() + try: + test_socket.bind(("", port)) + except OSError: + return False + + return True + + +def find_next_available_port(start_port: int): + """Find the next available port starting with the given port.""" + test_socket = _get_test_socket() + for port in range(start_port, MAX_PORT): + try: + test_socket.bind(("", port)) + return port + except OSError: + if port == MAX_PORT: + raise + continue + + +def pid_is_alive(pid): + """Check to see if a process is alive.""" + try: + os.kill(pid, 0) + return True + except OSError: + pass + return False diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 9e712b4127f..0b8f0b3b2f8 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -3,7 +3,7 @@ import logging from aiohomekit.model.characteristics import CharacteristicsTypes -from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -51,13 +51,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if service["stype"] != "security-system": return False info = {"aid": aid, "iid": service["iid"]} - async_add_entities([HomeKitAlarmControlPanel(conn, info)], True) + async_add_entities([HomeKitAlarmControlPanelEntity(conn, info)], True) return True conn.add_listener(async_add_service) -class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): +class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity): """Representation of a Homekit Alarm Control Panel.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index b96e5f651e3..939c6055e10 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.core import callback @@ -18,7 +18,7 @@ from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): +class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit motion sensor.""" def get_characteristic_types(self): @@ -36,7 +36,7 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice): return self.service.value(CharacteristicsTypes.MOTION_DETECTED) -class HomeKitContactSensor(HomeKitEntity, BinarySensorDevice): +class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit contact sensor.""" def get_characteristic_types(self): @@ -54,7 +54,7 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorDevice): return self.service.value(CharacteristicsTypes.CONTACT_STATE) == 1 -class HomeKitSmokeSensor(HomeKitEntity, BinarySensorDevice): +class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit smoke sensor.""" @property @@ -72,7 +72,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorDevice): return self.service.value(CharacteristicsTypes.SMOKE_DETECTED) == 1 -class HomeKitOccupancySensor(HomeKitEntity, BinarySensorDevice): +class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit occupancy sensor.""" @property @@ -90,7 +90,7 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorDevice): return self.service.value(CharacteristicsTypes.OCCUPANCY_DETECTED) == 1 -class HomeKitLeakSensor(HomeKitEntity, BinarySensorDevice): +class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit leak sensor.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 2262fa54770..f06063c5fd2 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -11,7 +11,7 @@ from aiohomekit.utils import clamp_enum_to_char from homeassistant.components.climate import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, - ClimateDevice, + ClimateEntity, ) from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, @@ -59,13 +59,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if service["stype"] != "thermostat": return False info = {"aid": aid, "iid": service["iid"]} - async_add_entities([HomeKitClimateDevice(conn, info)], True) + async_add_entities([HomeKitClimateEntity(conn, info)], True) return True conn.add_listener(async_add_service) -class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): +class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Representation of a Homekit climate device.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 88885d49b8e..086f780b816 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import callback @@ -58,7 +58,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn.add_listener(async_add_service) -class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): +class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): """Representation of a HomeKit Garage Door.""" @property @@ -128,7 +128,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): return attributes -class HomeKitWindowCover(HomeKitEntity, CoverDevice): +class HomeKitWindowCover(HomeKitEntity, CoverEntity): """Representation of a HomeKit Window or Window Covering.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index e78ed48ad0c..b024efe6121 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) from homeassistant.core import callback @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn.add_listener(async_add_service) -class HomeKitLight(HomeKitEntity, Light): +class HomeKitLight(HomeKitEntity, LightEntity): """Representation of a Homekit light.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index c07f85fb50f..93bb4f1568f 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -3,7 +3,7 @@ import logging from aiohomekit.model.characteristics import CharacteristicsTypes -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import callback @@ -34,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn.add_listener(async_add_service) -class HomeKitLock(HomeKitEntity, LockDevice): +class HomeKitLock(HomeKitEntity, LockEntity): """Representation of a HomeKit Controller Lock.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 3d5e194ed94..249b8c9c3e0 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -10,7 +10,7 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import ServicesTypes from aiohomekit.utils import clamp_enum_to_char -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerDevice +from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -54,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn.add_listener(async_add_service) -class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): +class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): """Representation of a HomeKit Controller Television.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 61595b504ca..32c7fd8515e 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -7,7 +7,7 @@ from aiohomekit.model.characteristics import ( IsConfiguredValues, ) -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -21,7 +21,7 @@ ATTR_IS_CONFIGURED = "is_configured" ATTR_REMAINING_DURATION = "remaining_duration" -class HomeKitSwitch(HomeKitEntity, SwitchDevice): +class HomeKitSwitch(HomeKitEntity, SwitchEntity): """Representation of a Homekit switch.""" def get_characteristic_types(self): @@ -49,7 +49,7 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): return {OUTLET_IN_USE: outlet_in_use} -class HomeKitValve(HomeKitEntity, SwitchDevice): +class HomeKitValve(HomeKitEntity, SwitchEntity): """Represents a valve in an irrigation system.""" def get_characteristic_types(self): diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index db174f049b8..3407d93c63f 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -13,7 +13,7 @@ "authentication_error": "Codi HomeKit incorrecte. Verifica'l i torna-ho a provar.", "busy_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 actualment ho est\u00e0 intentant amb un altre controlador diferent.", "max_peers_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 no t\u00e9 suficient espai lliure.", - "max_tries_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 ha rebut m\u00e9s de 100 intents d\u2019autenticaci\u00f3 fallits.", + "max_tries_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 ha rebut m\u00e9s de 100 intents d'autenticaci\u00f3 fallits.", "pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb el dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no estigui suportat.", "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." diff --git a/homeassistant/components/homekit_controller/translations/es-419.json b/homeassistant/components/homekit_controller/translations/es-419.json index f3a084e7545..a10c6eaa04f 100644 --- a/homeassistant/components/homekit_controller/translations/es-419.json +++ b/homeassistant/components/homekit_controller/translations/es-419.json @@ -1,10 +1,19 @@ { "config": { "abort": { + "accessory_not_found_error": "No se puede agregar el emparejamiento ya que el dispositivo ya no se puede encontrar.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", - "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo." + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", + "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo.", + "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que hay disponible una caracter\u00edstica m\u00e1s completa de integraci\u00f3n nativa.", + "invalid_config_entry": "Este dispositivo se muestra como listo para emparejar, pero ya hay una entrada de configuraci\u00f3n conflictiva en Home Assistant que primero debe eliminarse.", + "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { + "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor revisalo e int\u00e9ntalo de nuevo.", + "busy_error": "El dispositivo se neg\u00f3 a agregar el emparejamiento ya que ya se est\u00e1 emparejando con otro controlador.", + "max_peers_error": "El dispositivo se neg\u00f3 a agregar emparejamiento ya que no tiene almacenamiento de emparejamiento libre.", + "max_tries_error": "El dispositivo se neg\u00f3 a agregar el emparejamiento ya que recibi\u00f3 m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos.", "pairing_failed": "Se produjo un error no controlado al intentar vincularse con este dispositivo. Esto puede ser una falla temporal o su dispositivo puede no ser compatible actualmente.", "unable_to_pair": "No se puede vincular, por favor intente nuevamente.", "unknown_error": "El dispositivo inform\u00f3 un error desconocido. Vinculaci\u00f3n fallida." diff --git a/homeassistant/components/homekit_controller/translations/fi.json b/homeassistant/components/homekit_controller/translations/fi.json new file mode 100644 index 00000000000..cadab3a3da0 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "pair": { + "data": { + "pairing_code": "Yhdist\u00e4miskoodi" + } + }, + "user": { + "data": { + "device": "Laite" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json index d42001f629a..6cd6c20aad0 100644 --- a/homeassistant/components/homekit_controller/translations/ko.json +++ b/homeassistant/components/homekit_controller/translations/ko.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc73c\ubbc0\ub85c \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "already_configured": "\uc561\uc138\uc11c\ub9ac\uac00 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", "already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.", @@ -25,16 +25,16 @@ "pairing_code": "\ud398\uc5b4\ub9c1 \ucf54\ub4dc" }, "description": "\uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc (XXX-XX-XXX \ud615\uc2dd) \ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1" + "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30" }, "user": { "data": { "device": "\uae30\uae30" }, "description": "\ud398\uc5b4\ub9c1 \ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1" + "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30" } } }, - "title": "HomeKit \uc561\uc138\uc11c\ub9ac" + "title": "HomeKit \ucee8\ud2b8\ub864\ub7ec" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index f3d93fd9e92..72dfeede783 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -5,7 +5,7 @@ "already_configured": "Tilbeh\u00f8r er allerede konfigurert med denne kontrolleren.", "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.", "already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.", - "ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.", + "ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrasjon er tilgjengelig.", "invalid_config_entry": "Denne enheten vises som klar til sammenkobling, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Hjelpeassistenten som f\u00f8rst m\u00e5 fjernes.", "no_devices": "Ingen ukoblede enheter ble funnet" }, @@ -24,7 +24,7 @@ "data": { "pairing_code": "Sammenkoblingskode" }, - "description": "Skriv inn din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret", + "description": "Angi din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret", "title": "Koble til HomeKit tilbeh\u00f8r" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 4b58e4f9fb6..a8360161c6a 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -3,6 +3,7 @@ "abort": { "accessory_not_found_error": "\u65e0\u6cd5\u6dfb\u52a0\u914d\u5bf9\uff0c\u56e0\u4e3a\u65e0\u6cd5\u518d\u627e\u5230\u8bbe\u5907\u3002", "already_configured": "\u914d\u4ef6\u5df2\u901a\u8fc7\u6b64\u63a7\u5236\u5668\u914d\u7f6e\u5b8c\u6210\u3002", + "already_in_progress": "\u6b64\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002", "already_paired": "\u6b64\u914d\u4ef6\u5df2\u4e0e\u53e6\u4e00\u53f0\u8bbe\u5907\u914d\u5bf9\u3002\u8bf7\u91cd\u7f6e\u914d\u4ef6\uff0c\u7136\u540e\u91cd\u8bd5\u3002", "ignored_model": "HomeKit \u5bf9\u6b64\u8bbe\u5907\u7684\u652f\u6301\u5df2\u88ab\u963b\u6b62\uff0c\u56e0\u4e3a\u6709\u529f\u80fd\u66f4\u5b8c\u6574\u7684\u539f\u751f\u96c6\u6210\u53ef\u4ee5\u4f7f\u7528\u3002", "invalid_config_entry": "\u6b64\u8bbe\u5907\u5df2\u51c6\u5907\u597d\u914d\u5bf9\uff0c\u4f46\u662f Home Assistant \u4e2d\u5b58\u5728\u4e0e\u4e4b\u51b2\u7a81\u7684\u914d\u7f6e\uff0c\u5fc5\u987b\u5148\u5c06\u5176\u5220\u9664\u3002", diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 731525c8460..041f2f02643 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SMOKE, - BinarySensorDevice, + BinarySensorEntity, ) from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMBinarySensor(HMDevice, BinarySensorDevice): +class HMBinarySensor(HMDevice, BinarySensorEntity): """Representation of a binary HomeMatic device.""" @property @@ -73,7 +73,7 @@ class HMBinarySensor(HMDevice, BinarySensorDevice): self._data.update({self._state: None}) -class HMBatterySensor(HMDevice, BinarySensorDevice): +class HMBatterySensor(HMDevice, BinarySensorEntity): """Representation of an HomeMatic low battery sensor.""" @property diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index b4ab277a75b..243e1782a37 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -1,7 +1,7 @@ """Support for Homematic thermostats.""" import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMThermostat(HMDevice, ClimateDevice): +class HMThermostat(HMDevice, ClimateEntity): """Representation of a Homematic thermostat.""" @property diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 0dea1181d73..a520c08e478 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - CoverDevice, + CoverEntity, ) from .const import ATTR_DISCOVER_DEVICES @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMCover(HMDevice, CoverDevice): +class HMCover(HMDevice, CoverEntity): """Representation a HomeMatic Cover.""" @property diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 6e6ccb78371..c7cfcc2ac8c 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -11,7 +11,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - Light, + LightEntity, ) from .const import ATTR_DISCOVER_DEVICES @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMLight(HMDevice, Light): +class HMLight(HMDevice, LightEntity): """Representation of a Homematic light.""" @property diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 0094ecd2e81..9a705627fa4 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -1,7 +1,7 @@ """Support for Homematic locks.""" import logging -from homeassistant.components.lock import SUPPORT_OPEN, LockDevice +from homeassistant.components.lock import SUPPORT_OPEN, LockEntity from .const import ATTR_DISCOVER_DEVICES from .entity import HMDevice @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMLock(HMDevice, LockDevice): +class HMLock(HMDevice, LockEntity): """Representation of a Homematic lock aka KeyMatic.""" @property diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 7c4486065b4..da803b406bf 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -23,6 +23,7 @@ from .entity import HMDevice _LOGGER = logging.getLogger(__name__) HM_STATE_HA_CAST = { + "IPGarage": {0: "closed", 1: "open", 2: "ventilation", 3: None}, "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"}, "RotaryHandleSensorIP": {0: "closed", 1: "tilted", 2: "open"}, "WaterSensor": {0: "dry", 1: "wet", 2: "water"}, diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index 53679818083..afde1ac8527 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -1,7 +1,7 @@ """Support for HomeMatic switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from .const import ATTR_DISCOVER_DEVICES from .entity import HMDevice @@ -22,7 +22,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMSwitch(HMDevice, SwitchDevice): +class HMSwitch(HMDevice, SwitchEntity): """Representation of a HomeMatic switch.""" @property diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index fd3958344f5..7e06cd60536 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -4,7 +4,7 @@ from typing import Any, Dict from homematicip.functionalHomes import SecurityAndAlarmHome -from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -32,10 +32,10 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - async_add_entities([HomematicipAlarmControlPanel(hap)]) + async_add_entities([HomematicipAlarmControlPanelEntity(hap)]) -class HomematicipAlarmControlPanel(AlarmControlPanel): +class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): """Representation of an alarm control panel.""" def __init__(self, hap: HomematicipHAP) -> None: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 00147e1b7ec..15c41be24b5 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -36,7 +36,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -131,7 +131,7 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud acceleration sensor.""" @property @@ -157,7 +157,7 @@ class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice return state_attr -class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud contact interface.""" @property @@ -173,7 +173,7 @@ class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): return self._device.windowState != WindowState.CLOSED -class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud shutter contact.""" @property @@ -189,7 +189,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): return self._device.windowState != WindowState.CLOSED -class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud motion detector.""" @property @@ -203,7 +203,7 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): return self._device.motionDetected -class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud presence detector.""" @property @@ -217,7 +217,7 @@ class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorDevice): return self._device.presenceDetected -class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud smoke detector.""" @property @@ -236,7 +236,7 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): return False -class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud water detector.""" @property @@ -250,7 +250,7 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): return self._device.moistureDetected or self._device.waterlevelDetected -class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud storm sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -268,7 +268,7 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): return self._device.storm -class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud rain sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -286,7 +286,7 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): return self._device.raining -class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud sunshine sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -315,7 +315,7 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): return state_attr -class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud low battery sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -334,7 +334,7 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipPluggableMainsFailureSurveillanceSensor( - HomematicipGenericDevice, BinarySensorDevice + HomematicipGenericDevice, BinarySensorEntity ): """Representation of a HomematicIP Cloud pluggable mains failure surveillance sensor.""" @@ -353,7 +353,7 @@ class HomematicipPluggableMainsFailureSurveillanceSensor( return not self._device.powerMainsFailure -class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): +class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorEntity): """Representation of a HomematicIP Cloud security zone group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None: @@ -409,7 +409,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD class HomematicipSecuritySensorGroup( - HomematicipSecurityZoneSensorGroup, BinarySensorDevice + HomematicipSecurityZoneSensorGroup, BinarySensorEntity ): """Representation of a HomematicIP security group.""" diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index c5fb978e690..3d01f5d69fd 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -8,7 +8,7 @@ from homematicip.base.enums import AbsenceType from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -57,7 +57,7 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): +class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateEntity): """Representation of a HomematicIP heating group. Heat mode is supported for all heating devices incl. their defined profiles. diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 768c893a100..580e2d21a11 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -13,7 +13,7 @@ from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - CoverDevice, + CoverEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -51,7 +51,7 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): +class HomematicipCoverShutter(HomematicipGenericDevice, CoverEntity): """Representation of a HomematicIP Cloud cover shutter device.""" @property @@ -88,7 +88,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): await self._device.set_shutter_stop() -class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): +class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): """Representation of a HomematicIP Cloud cover slats device.""" @property @@ -118,7 +118,7 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): await self._device.set_shutter_stop() -class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice): +class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverEntity): """Representation of a HomematicIP Garage Door Module for Tormatic.""" @property @@ -150,7 +150,7 @@ class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice) await self._device.send_door_command(DoorCommand.STOP) -class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverDevice): +class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverEntity): """Representation of a HomematicIP Cloud cover shutter group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 42c18239ac2..9ddcc44e8bd 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -64,7 +64,7 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipLight(HomematicipGenericDevice, Light): +class HomematicipLight(HomematicipGenericDevice, LightEntity): """Representation of a HomematicIP Cloud light device.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -110,7 +110,7 @@ class HomematicipLightMeasuring(HomematicipLight): return state_attr -class HomematicipDimmer(HomematicipGenericDevice, Light): +class HomematicipDimmer(HomematicipGenericDevice, LightEntity): """Representation of HomematicIP Cloud dimmer light device.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -144,7 +144,7 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): await self._device.set_dim_level(0) -class HomematicipNotificationLight(HomematicipGenericDevice, Light): +class HomematicipNotificationLight(HomematicipGenericDevice, LightEntity): """Representation of HomematicIP Cloud dimmer light device.""" def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 79f7b9dfa5c..f000aef0695 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -15,7 +15,7 @@ from homematicip.aio.device import ( ) from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -67,7 +67,7 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): +class HomematicipSwitch(HomematicipGenericDevice, SwitchEntity): """representation of a HomematicIP Cloud switch device.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -88,7 +88,7 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): await self._device.turn_off() -class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): +class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchEntity): """representation of a HomematicIP switching group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None: @@ -145,7 +145,7 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): return round(self._device.energyCounter) -class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): +class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchEntity): """Representation of a HomematicIP Cloud multi switch device.""" def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: diff --git a/homeassistant/components/homematicip_cloud/translations/ca.json b/homeassistant/components/homematicip_cloud/translations/ca.json index 4cb5e8d092d..377b6e53a40 100644 --- a/homeassistant/components/homematicip_cloud/translations/ca.json +++ b/homeassistant/components/homematicip_cloud/translations/ca.json @@ -21,7 +21,7 @@ "title": "Tria el punt d'acc\u00e9s HomematicIP" }, "link": { - "description": "Prem el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 Envia per registrar HomematicIP amb Home Assistent. \n\n![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Prem el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 Envia per registrar HomematicIP amb Home Assistent. \n\n![Ubicaci\u00f3 del bot\u00f3 a l'element d'enlla\u00e7](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Enlla\u00e7 amb punt d'acc\u00e9s" } } diff --git a/homeassistant/components/homematicip_cloud/translations/es-419.json b/homeassistant/components/homematicip_cloud/translations/es-419.json index a853d7677c8..1b743f1c51f 100644 --- a/homeassistant/components/homematicip_cloud/translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/translations/es-419.json @@ -8,7 +8,8 @@ "error": { "invalid_pin": "PIN no v\u00e1lido, por favor intente de nuevo.", "press_the_button": "Por favor, presione el bot\u00f3n azul.", - "register_failed": "No se pudo registrar, por favor intente de nuevo." + "register_failed": "No se pudo registrar, por favor intente de nuevo.", + "timeout_button": "Tiempo de espera del bot\u00f3n azul, intente nuevamente." }, "step": { "init": { @@ -20,7 +21,8 @@ "title": "Elija el punto de acceso HomematicIP" }, "link": { - "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)" + "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Enlazar Punto de acceso" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index a017a5a1df7..b1d1ea64e3f 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -21,7 +21,7 @@ "title": "Elegir punto de acceso HomematicIP" }, "link": { - "description": "Pulsa el bot\u00f3n azul en el punto de acceso y el bot\u00f3n de env\u00edo para registrar HomematicIP en Home Assistant.\n\n![Ubicaci\u00f3n del bot\u00f3n en el puente](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Pulsa el bot\u00f3n azul en el punto de acceso y el bot\u00f3n de env\u00edo para registrar HomematicIP en Home Assistant.\n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Enlazar punto de acceso" } } diff --git a/homeassistant/components/homematicip_cloud/translations/fi.json b/homeassistant/components/homematicip_cloud/translations/fi.json new file mode 100644 index 00000000000..fe5cc461992 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "unknown": "Tapahtui tuntematon virhe." + }, + "error": { + "invalid_pin": "Virheellinen PIN-koodi, yrit\u00e4 uudelleen.", + "press_the_button": "Paina sinist\u00e4 painiketta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json index 954aff55b63..733fc9ebf51 100644 --- a/homeassistant/components/homematicip_cloud/translations/ko.json +++ b/homeassistant/components/homematicip_cloud/translations/ko.json @@ -18,11 +18,11 @@ "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uae30\uae30 \uc774\ub984\uc758 \uc811\ub450\uc5b4\ub85c \uc0ac\uc6a9)", "pin": "PIN \ucf54\ub4dc (\uc120\ud0dd\uc0ac\ud56d)" }, - "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd" + "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd\ud558\uae30" }, "link": { - "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc5f0\uacb0" + "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json index 7c0497c2d43..2229348efa1 100644 --- a/homeassistant/components/homematicip_cloud/translations/pl.json +++ b/homeassistant/components/homematicip_cloud/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.", "connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", - "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index db72c87a4a3..a5a3b9ed077 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -3,7 +3,11 @@ import logging from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -28,7 +32,7 @@ def setup_platform(hass, config, add_entities, discover_info=None): add_entities(devs, True) -class HomeworksLight(HomeworksDevice, Light): +class HomeworksLight(HomeworksDevice, LightEntity): """Homeworks Light.""" def __init__(self, controller, addr, name, rate): diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index ece8257a713..5969dcdcc27 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -7,7 +7,7 @@ import requests import somecomfort import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -145,7 +145,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class HoneywellUSThermostat(ClimateDevice): +class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" def __init__( diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 44e93d26a40..0ed98f73a38 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -7,7 +7,7 @@ from horimote.exceptions import AuthenticationError import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -78,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([HorizonDevice(client, name, keys)], True) -class HorizonDevice(MediaPlayerDevice): +class HorizonDevice(MediaPlayerEntity): """Representation of a Horizon HD Recorder.""" def __init__(self, client, name, remote_keys): @@ -116,12 +116,12 @@ class HorizonDevice(MediaPlayerDevice): def turn_on(self): """Turn the device on.""" - if self._state is STATE_OFF: + if self._state == STATE_OFF: self._send_key(self._keys.POWER) def turn_off(self): """Turn the device off.""" - if self._state is not STATE_OFF: + if self._state != STATE_OFF: self._send_key(self._keys.POWER) def media_previous_track(self): diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 565f84fdb8a..069fc42c884 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -3,6 +3,7 @@ from ipaddress import ip_network import logging import os import ssl +from traceback import extract_stack from typing import Optional, cast from aiohttp import web @@ -63,29 +64,32 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -HTTP_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, - vol.Optional(CONF_BASE_URL): cv.string, - vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_KEY): cv.isfile, - vol.Optional(CONF_CORS_ORIGINS, default=[DEFAULT_CORS]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean, - vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All( - cv.ensure_list, [ip_network] - ), - vol.Optional( - CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD - ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), - vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( - [SSL_INTERMEDIATE, SSL_MODERN] - ), - } +HTTP_SCHEMA = vol.All( + cv.deprecated(CONF_BASE_URL), + vol.Schema( + { + vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, + vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, + vol.Optional(CONF_BASE_URL): cv.string, + vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_KEY): cv.isfile, + vol.Optional(CONF_CORS_ORIGINS, default=[DEFAULT_CORS]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Inclusive(CONF_USE_X_FORWARDED_FOR, "proxy"): cv.boolean, + vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All( + cv.ensure_list, [ip_network] + ), + vol.Optional( + CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD + ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), + vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, + vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( + [SSL_INTERMEDIATE, SSL_MODERN] + ), + } + ), ) CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) @@ -102,23 +106,80 @@ class ApiConfig: """Configuration settings for API server.""" def __init__( - self, host: str, port: Optional[int] = SERVER_PORT, use_ssl: bool = False + self, + local_ip: str, + host: str, + port: Optional[int] = SERVER_PORT, + use_ssl: bool = False, ) -> None: """Initialize a new API config object.""" + self.local_ip = local_ip self.host = host self.port = port self.use_ssl = use_ssl host = host.rstrip("/") if host.startswith(("http://", "https://")): - self.base_url = host + self.deprecated_base_url = host elif use_ssl: - self.base_url = f"https://{host}" + self.deprecated_base_url = f"https://{host}" else: - self.base_url = f"http://{host}" + self.deprecated_base_url = f"http://{host}" if port is not None: - self.base_url += f":{port}" + self.deprecated_base_url += f":{port}" + + @property + def base_url(self) -> str: + """Proxy property to find caller of this deprecated property.""" + found_frame = None + for frame in reversed(extract_stack()): + for path in ("custom_components/", "homeassistant/components/"): + try: + index = frame.filename.index(path) + + # Skip webhook from the stack + if frame.filename[index:].startswith( + "homeassistant/components/webhook/" + ): + continue + + found_frame = frame + break + except ValueError: + continue + + if found_frame is not None: + break + + # Did not source from an integration? Hard error. + if found_frame is None: + raise RuntimeError( + "Detected use of deprecated `base_url` property in the Home Assistant core. Please report this issue." + ) + + # If a frame was found, it originated from an integration + if found_frame: + start = index + len(path) + end = found_frame.filename.index("/", start) + + integration = found_frame.filename[start:end] + + if path == "custom_components/": + extra = " to the custom component author" + else: + extra = "" + + _LOGGER.warning( + "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue%s for %s using this method at %s, line %s: %s", + extra, + integration, + found_frame.filename[index:], + found_frame.lineno, + found_frame.line.strip(), + ) + + return self.deprecated_base_url async def async_setup(hass, config): @@ -182,6 +243,7 @@ async def async_setup(hass, config): hass.http = server host = conf.get(CONF_BASE_URL) + local_ip = await hass.async_add_executor_job(hass_util.get_local_ip) if host: port = None @@ -189,10 +251,10 @@ async def async_setup(hass, config): host = server_host port = server_port else: - host = hass_util.get_local_ip() + host = local_ip port = server_port - hass.config.api = ApiConfig(host, port, ssl_certificate is not None) + hass.config.api = ApiConfig(local_ip, host, port, ssl_certificate is not None) return True diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index af6ed75d591..575cc9789ca 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -8,7 +8,7 @@ from huawei_lte_api.enums.cradle import ConnectionStatusEnum from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import CONF_URL @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @attr.s -class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorDevice): +class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorEntity): """Huawei LTE binary sensor device base class.""" key: str diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index e1e91c08e9a..0660aa361f5 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.2", - "huawei-lte-api==1.4.11", + "huawei-lte-api==1.4.12", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index e3a89b8f418..19a37757d5f 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -19,9 +19,9 @@ "step": { "user": { "data": { - "password": "Password", + "password": "[%key:common::config_flow::data::password%]", "url": "URL", - "username": "User name" + "username": "[%key:common::config_flow::data::username%]" }, "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", "title": "Configure Huawei LTE" diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 44d2da0c898..45b179f470f 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -8,7 +8,7 @@ import attr from homeassistant.components.switch import ( DEVICE_CLASS_SWITCH, DOMAIN as SWITCH_DOMAIN, - SwitchDevice, + SwitchEntity, ) from homeassistant.const import CONF_URL @@ -30,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @attr.s -class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchDevice): +class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity): """Huawei LTE switch device base class.""" key: str diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 762132a459a..cb8a4331a57 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Nom d'usuari" }, - "description": "Introdueix les dades d\u2019acc\u00e9s del dispositiu. El nom d\u2019usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", + "description": "Introdueix les dades d'acc\u00e9s del dispositiu. El nom d'usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", "title": "Configuraci\u00f3 de Huawei LTE" } } diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 3a66c447e54..4496759d3ac 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -21,7 +21,7 @@ "data": { "password": "Password", "url": "URL", - "username": "User name" + "username": "Username" }, "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", "title": "Configure Huawei LTE" diff --git a/homeassistant/components/huawei_lte/translations/es-419.json b/homeassistant/components/huawei_lte/translations/es-419.json new file mode 100644 index 00000000000..a00f805c9fa --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/es-419.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo ya ha sido configurado", + "already_in_progress": "Este dispositivo ya est\u00e1 siendo configurado", + "not_huawei_lte": "No es un dispositivo Huawei LTE" + }, + "error": { + "connection_failed": "La conexi\u00f3n fall\u00f3", + "connection_timeout": "El tiempo de conexi\u00f3n expir\u00f3", + "incorrect_password": "Contrase\u00f1a incorrecta", + "incorrect_username": "Nombre de usuario incorrecto", + "incorrect_username_or_password": "Nombre de usuario o contrase\u00f1a incorrecta", + "invalid_url": "URL invalida", + "login_attempts_exceeded": "Se han excedido los intentos de inicio de sesi\u00f3n m\u00e1ximos. Vuelva a intentarlo m\u00e1s tarde", + "response_error": "Error desconocido del dispositivo", + "unknown_connection_error": "Error desconocido al conectarse al dispositivo" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Nombre de usuario" + }, + "description": "Ingrese los detalles de acceso del dispositivo. Especificar nombre de usuario y contrase\u00f1a es opcional, pero habilita el soporte para m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa y viceversa.", + "title": "Configurar Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", + "recipient": "Destinatarios de notificaciones por SMS", + "track_new_devices": "Rastrear nuevos dispositivos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 28885b9435d..ba39176d04a 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d8" + "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { "connection_failed": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", @@ -24,7 +24,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d \ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654 \ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4\ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "Huawei LTE \uc124\uc815" + "title": "Huawei LTE \uc124\uc815\ud558\uae30" } } }, diff --git a/homeassistant/components/huawei_lte/translations/lb.json b/homeassistant/components/huawei_lte/translations/lb.json index 25846f40b7c..16c8c8eaafc 100644 --- a/homeassistant/components/huawei_lte/translations/lb.json +++ b/homeassistant/components/huawei_lte/translations/lb.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert", "already_in_progress": "D\u00ebsen Apparat g\u00ebtt scho konfigur\u00e9iert", - "not_huawei_lte": "Ken Huawei LTE Apparat" + "not_huawei_lte": "Keen Huawei LTE Apparat" }, "error": { "connection_failed": "Feeler bei der Verbindung", diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 77c212c10b8..99dc194763c 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -8,7 +8,7 @@ "error": { "connection_failed": "Tilkoblingen mislyktes", "connection_timeout": "Tilkoblingsavbrudd", - "incorrect_password": "feil passord", + "incorrect_password": "Feil passord", "incorrect_username": "Feil brukernavn", "incorrect_username_or_password": "Feil brukernavn eller passord", "invalid_url": "Ugyldig URL-adresse", @@ -23,7 +23,7 @@ "url": "", "username": "Brukernavn" }, - "description": "Angi detaljer for enhetstilgang. Angivelse av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integreringsfunksjoner. P\u00e5 den annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integreringen er aktiv, og omvendt.", + "description": "Fyll inn detaljer for enhetstilgang. Spesifisering av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integrasjonsfunksjoner. P\u00e5 en annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integrasjonen er aktiv, og omvendt.", "title": "Konfigurer Huawei LTE" } } diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 86e6e4e5853..57768de8fc1 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -19,9 +19,9 @@ "step": { "user": { "data": { - "password": "Has\u0142o", + "password": "[%key_id:common::config_flow::data::password%]", "url": "URL", - "username": "Nazwa u\u017cytkownika" + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistant'a gdy integracja jest aktywna.", "title": "Konfiguracja Huawei LTE" diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 8a6b5d203a8..cfbe041aafe 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -4,7 +4,7 @@ from aiohue.sensors import TYPE_ZLL_PRESENCE from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, - BinarySensorDevice, + BinarySensorEntity, ) from .const import DOMAIN as HUE_DOMAIN @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ].sensor_manager.async_register_component("binary_sensor", async_add_entities) -class HuePresence(GenericZLLSensor, BinarySensorDevice): +class HuePresence(GenericZLLSensor, BinarySensorEntity): """The presence sensor entity for a Hue motion sensor device.""" device_class = DEVICE_CLASS_MOTION diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 6157a7fde23..c9d543dba94 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -24,7 +24,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady @@ -194,7 +194,7 @@ def hass_to_hue_brightness(value): return max(1, round((value / 255) * 254)) -class HueLight(Light): +class HueLight(LightEntity): """Representation of a Hue light.""" def __init__(self, coordinator, bridge, is_group, light, supported_features): diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index c912e9f7f0d..5d56a787448 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -4,7 +4,7 @@ "init": { "title": "Pick Hue bridge", "data": { - "host": "Host" + "host": "[%key:common::config_flow::data::host%]" } }, "link": { @@ -48,4 +48,4 @@ "remote_double_button_short_press": "Both \"{subtype}\" released" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 77686f5a272..3294cd2f0e8 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -22,7 +22,7 @@ "title": "Tria de l'enlla\u00e7 Hue" }, "link": { - "description": "Prem el bot\u00f3 de l'enlla\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)", + "description": "Prem el bot\u00f3 de l'enlla\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 a l'element d'enlla\u00e7](/static/images/config_philips_hue.jpg)", "title": "Vincular concentrador" } } diff --git a/homeassistant/components/hue/translations/es-419.json b/homeassistant/components/hue/translations/es-419.json index 8b2b90456bd..ec81c24e2cb 100644 --- a/homeassistant/components/hue/translations/es-419.json +++ b/homeassistant/components/hue/translations/es-419.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", "already_configured": "El puente ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "cannot_connect": "No se puede conectar al puente", "discover_timeout": "Incapaz de descubrir puentes Hue", "no_bridges": "No se descubrieron puentes Philips Hue", @@ -17,8 +18,34 @@ "init": { "data": { "host": "Host" - } + }, + "title": "Seleccione el puente Hue" + }, + "link": { + "description": "Presione el bot\u00f3n en el puente para registrar Philips Hue con Home Assistant. \n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "Enlazar Hub" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "dim_down": "Bajar la intensidad", + "dim_up": "Aumentar intensidad", + "double_buttons_1_3": "Botones primero y tercero", + "double_buttons_2_4": "Botones segundo y cuarto", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_short_press": "Se presion\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_short_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_double_button_long_press": "Ambos \"{subtype}\" sueltos despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_double_button_short_press": "Ambos \"{subtype}\" sueltos" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index ddaad5c424a..f4762560485 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", - "already_configured": "El puente ya esta configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", - "cannot_connect": "No se puede conectar al puente", - "discover_timeout": "No se han descubierto puentes Philips Hue", - "no_bridges": "No se han descubierto puentes Philips Hue.", - "not_hue_bridge": "No es un puente Hue", + "all_configured": "Ya se han configurado todas las pasarelas Philips Hue", + "already_configured": "La pasarela ya esta configurada", + "already_in_progress": "La configuraci\u00f3n del flujo para la pasarela ya est\u00e1 en curso.", + "cannot_connect": "No se puede conectar a la pasarela", + "discover_timeout": "Imposible encontrar pasarelas Philips Hue", + "no_bridges": "No se han encontrado pasarelas Philips Hue.", + "not_hue_bridge": "No es una pasarela Hue", "unknown": "Se produjo un error desconocido" }, "error": { @@ -19,10 +19,10 @@ "data": { "host": "Host" }, - "title": "Elige el puente de Hue" + "title": "Elige la pasarela Hue" }, "link": { - "description": "Presione el bot\u00f3n en el puente para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en el puente](/static/images/config_philips_hue.jpg)", + "description": "Presione el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", "title": "Link Hub" } } diff --git a/homeassistant/components/hue/translations/fi.json b/homeassistant/components/hue/translations/fi.json new file mode 100644 index 00000000000..ed539c71cb4 --- /dev/null +++ b/homeassistant/components/hue/translations/fi.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "Kaikki Philips Hue -sillat on jo m\u00e4\u00e4ritetty", + "already_configured": "Silta on jo m\u00e4\u00e4ritetty", + "cannot_connect": "Yhdist\u00e4minen siltaan ei onnistu", + "discover_timeout": "Hue-siltoja ei l\u00f6ydy", + "no_bridges": "Philips Hue -siltoja ei l\u00f6ydy", + "not_hue_bridge": "Ei Hue-silta", + "unknown": "Tapahtui tuntematon virhe" + }, + "error": { + "linking": "Tuntematon linkitysvirhe.", + "register_failed": "Rekister\u00f6inti ep\u00e4onnistui. Yrit\u00e4 uudelleen" + }, + "step": { + "init": { + "data": { + "host": "Palvelin" + }, + "title": "Valitse Hue-silta" + }, + "link": { + "title": "Link Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 80e90f16be0..c3e3203a66c 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -24,5 +24,11 @@ "title": "Kapcsol\u00f3d\u00e1s a hubhoz" } } + }, + "device_automation": { + "trigger_subtype": { + "turn_off": "Kikapcsol\u00e1s", + "turn_on": "Bekapcsol\u00e1s" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json index 561b3442d2e..62a466565d9 100644 --- a/homeassistant/components/hue/translations/ko.json +++ b/homeassistant/components/hue/translations/ko.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\ube0c\ub9bf\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "not_hue_bridge": "Hue \ube0c\ub9bf\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "discover_timeout": "Hue \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_hue_bridge": "Hue \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -19,11 +19,11 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "title": "Hue \ube0c\ub9bf\uc9c0 \uc120\ud0dd" + "title": "Hue \ube0c\ub9ac\uc9c0 \uc120\ud0dd\ud558\uae30" }, "link": { - "description": "\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)", - "title": "\ud5c8\ube0c \uc5f0\uacb0" + "description": "\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)", + "title": "\ud5c8\ube0c \uc5f0\uacb0\ud558\uae30" } } }, diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 346bc8af9ac..bfe70ae53ab 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -33,8 +33,19 @@ "button_2": "Tweede knop", "button_3": "Derde knop", "button_4": "Vierde knop", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "double_buttons_1_3": "Eerste en derde knop", + "double_buttons_2_4": "Tweede en vierde knop", "turn_off": "Uitschakelen", "turn_on": "Inschakelen" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang drukken", + "remote_button_short_press": "\"{subtype}\" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_double_button_long_press": "Beide \"{subtype}\" vrijgegeven na lang indrukken", + "remote_double_button_short_press": "Beide \"{subtype}\" vrijgegeven" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index b1f635026e6..acd2161a024 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -8,7 +8,7 @@ "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue", "not_hue_bridge": "To nie jest mostek Hue", - "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "host": "Nazwa hosta lub adres IP" + "host": "[%key_id:common::config_flow::data::host%]" }, "title": "Wybierz mostek Hue" }, diff --git a/homeassistant/components/hue/translations/sl.json b/homeassistant/components/hue/translations/sl.json index cefd7393f2a..a8d88ec49ac 100644 --- a/homeassistant/components/hue/translations/sl.json +++ b/homeassistant/components/hue/translations/sl.json @@ -35,13 +35,17 @@ "button_4": "\u010cetrti gumb", "dim_down": "Zatemnite", "dim_up": "pove\u010dajte mo\u010d", + "double_buttons_1_3": "Prvi in tretji gumb", + "double_buttons_2_4": "Drugi in \u010detrti gumb", "turn_off": "Ugasni", "turn_on": "Pri\u017egi" }, "trigger_type": { "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", - "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den" + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_double_button_long_press": "Po dolgem pritisku sta obe \" {subtype} \" spro\u0161\u010deni", + "remote_double_button_short_press": "Spro\u0161\u010dena oba \"{podvrsta}\"" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 894c4f9f988..80f7b179692 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -26,5 +26,22 @@ "title": "L\u00e4nka hub" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", + "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt" + } } } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 14ede545576..89dc610a6fc 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -1 +1,194 @@ -"""The hunterdouglas_powerview component.""" +"""The Hunter Douglas PowerView integration.""" +import asyncio +from datetime import timedelta +import logging + +from aiopvapi.helpers.aiorequest import AioRequest +from aiopvapi.helpers.constants import ATTR_ID +from aiopvapi.helpers.tools import base64_to_unicode +from aiopvapi.rooms import Rooms +from aiopvapi.scenes import Scenes +from aiopvapi.shades import Shades +from aiopvapi.userdata import UserData +import async_timeout +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + COORDINATOR, + DEVICE_FIRMWARE, + DEVICE_INFO, + DEVICE_MAC_ADDRESS, + DEVICE_MODEL, + DEVICE_NAME, + DEVICE_REVISION, + DEVICE_SERIAL_NUMBER, + DOMAIN, + FIRMWARE_IN_USERDATA, + HUB_EXCEPTIONS, + HUB_NAME, + MAC_ADDRESS_IN_USERDATA, + MAINPROCESSOR_IN_USERDATA_FIRMWARE, + MODEL_IN_MAINPROCESSOR, + PV_API, + PV_ROOM_DATA, + PV_SCENE_DATA, + PV_SHADE_DATA, + PV_SHADES, + REVISION_IN_MAINPROCESSOR, + ROOM_DATA, + SCENE_DATA, + SERIAL_NUMBER_IN_USERDATA, + SHADE_DATA, + USER_DATA, +) + +DEVICE_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) + + +def _has_all_unique_hosts(value): + """Validate that each hub configured has a unique host.""" + hosts = [device[CONF_HOST] for device in value] + schema = vol.Schema(vol.Unique()) + schema(hosts) + return value + + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_hosts)}, + extra=vol.ALLOW_EXTRA, +) + + +PLATFORMS = ["cover", "scene", "sensor"] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, hass_config: dict): + """Set up the Hunter Douglas PowerView component.""" + hass.data.setdefault(DOMAIN, {}) + + if DOMAIN not in hass_config: + return True + + for conf in hass_config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Hunter Douglas PowerView from a config entry.""" + + config = entry.data + + hub_address = config.get(CONF_HOST) + websession = async_get_clientsession(hass) + + pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) + + try: + async with async_timeout.timeout(10): + device_info = await async_get_device_info(pv_request) + except HUB_EXCEPTIONS: + _LOGGER.error("Connection error to PowerView hub: %s", hub_address) + raise ConfigEntryNotReady + if not device_info: + _LOGGER.error("Unable to initialize PowerView hub: %s", hub_address) + raise ConfigEntryNotReady + + rooms = Rooms(pv_request) + room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) + + scenes = Scenes(pv_request) + scene_data = _async_map_data_by_id((await scenes.get_resources())[SCENE_DATA]) + + shades = Shades(pv_request) + shade_data = _async_map_data_by_id((await shades.get_resources())[SHADE_DATA]) + + async def async_update_data(): + """Fetch data from shade endpoint.""" + async with async_timeout.timeout(10): + shade_entries = await shades.get_resources() + if not shade_entries: + raise UpdateFailed(f"Failed to fetch new shade data.") + return _async_map_data_by_id(shade_entries[SHADE_DATA]) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="powerview hub", + update_method=async_update_data, + update_interval=timedelta(seconds=60), + ) + + hass.data[DOMAIN][entry.entry_id] = { + PV_API: pv_request, + PV_ROOM_DATA: room_data, + PV_SCENE_DATA: scene_data, + PV_SHADES: shades, + PV_SHADE_DATA: shade_data, + COORDINATOR: coordinator, + DEVICE_INFO: device_info, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_get_device_info(pv_request): + """Determine device info.""" + userdata = UserData(pv_request) + resources = await userdata.get_resources() + userdata_data = resources[USER_DATA] + + main_processor_info = userdata_data[FIRMWARE_IN_USERDATA][ + MAINPROCESSOR_IN_USERDATA_FIRMWARE + ] + return { + DEVICE_NAME: base64_to_unicode(userdata_data[HUB_NAME]), + DEVICE_MAC_ADDRESS: userdata_data[MAC_ADDRESS_IN_USERDATA], + DEVICE_SERIAL_NUMBER: userdata_data[SERIAL_NUMBER_IN_USERDATA], + DEVICE_REVISION: main_processor_info[REVISION_IN_MAINPROCESSOR], + DEVICE_FIRMWARE: main_processor_info, + DEVICE_MODEL: main_processor_info[MODEL_IN_MAINPROCESSOR], + } + + +@callback +def _async_map_data_by_id(data): + """Return a dict with the key being the id for a list of entries.""" + return {entry[ATTR_ID]: entry for entry in data} + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py new file mode 100644 index 00000000000..1e47b9ec3fe --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -0,0 +1,137 @@ +"""Config flow for Hunter Douglas PowerView integration.""" +import logging + +from aiopvapi.helpers.aiorequest import AioRequest +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from . import async_get_device_info +from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, HUB_EXCEPTIONS +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) +HAP_SUFFIX = "._hap._tcp.local." + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + hub_address = data[CONF_HOST] + websession = async_get_clientsession(hass) + + pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) + + try: + async with async_timeout.timeout(10): + device_info = await async_get_device_info(pv_request) + except HUB_EXCEPTIONS: + raise CannotConnect + if not device_info: + raise CannotConnect + + # Return info that you want to store in the config entry. + return { + "title": device_info[DEVICE_NAME], + "unique_id": device_info[DEVICE_SERIAL_NUMBER], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Hunter Douglas PowerView.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the powerview config flow.""" + self.powerview_config = {} + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if self._host_already_configured(user_input[CONF_HOST]): + return self.async_abort(reason="already_configured") + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(info["unique_id"]) + return self.async_create_entry( + title=info["title"], data={CONF_HOST: user_input[CONF_HOST]} + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input=None): + """Handle the initial step.""" + return await self.async_step_user(user_input) + + async def async_step_homekit(self, homekit_info): + """Handle HomeKit discovery.""" + + # If we already have the host configured do + # not open connections to it if we can avoid it. + if self._host_already_configured(homekit_info[CONF_HOST]): + return self.async_abort(reason="already_configured") + + try: + info = await validate_input(self.hass, homekit_info) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + return self.async_abort(reason="unknown") + + await self.async_set_unique_id(info["unique_id"], raise_on_progress=False) + self._abort_if_unique_id_configured({CONF_HOST: homekit_info["host"]}) + + name = homekit_info["name"] + if name.endswith(HAP_SUFFIX): + name = name[: -len(HAP_SUFFIX)] + + self.powerview_config = { + CONF_HOST: homekit_info["host"], + CONF_NAME: name, + } + return await self.async_step_link() + + async def async_step_link(self, user_input=None): + """Attempt to link with Powerview.""" + if user_input is not None: + return self.async_create_entry( + title=self.powerview_config[CONF_NAME], + data={CONF_HOST: self.powerview_config[CONF_HOST]}, + ) + + return self.async_show_form( + step_id="link", description_placeholders=self.powerview_config + ) + + def _host_already_configured(self, host): + """See if we already have a hub with the host address configured.""" + existing_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries() + if CONF_HOST in entry.data + } + return host in existing_hosts + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py new file mode 100644 index 00000000000..17ff3821a7a --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -0,0 +1,67 @@ +"""Support for Powerview scenes from a Powerview hub.""" + +import asyncio + +from aiopvapi.helpers.aiorequest import PvApiConnectionError + +DOMAIN = "hunterdouglas_powerview" + + +MANUFACTURER = "Hunter Douglas" + +HUB_ADDRESS = "address" + +SCENE_DATA = "sceneData" +SHADE_DATA = "shadeData" +ROOM_DATA = "roomData" +USER_DATA = "userData" + +MAC_ADDRESS_IN_USERDATA = "macAddress" +SERIAL_NUMBER_IN_USERDATA = "serialNumber" +FIRMWARE_IN_USERDATA = "firmware" +MAINPROCESSOR_IN_USERDATA_FIRMWARE = "mainProcessor" +REVISION_IN_MAINPROCESSOR = "revision" +MODEL_IN_MAINPROCESSOR = "name" +HUB_NAME = "hubName" + +FIRMWARE_IN_SHADE = "firmware" + +FIRMWARE_REVISION = "revision" +FIRMWARE_SUB_REVISION = "subRevision" +FIRMWARE_BUILD = "build" + +DEVICE_NAME = "device_name" +DEVICE_MAC_ADDRESS = "device_mac_address" +DEVICE_SERIAL_NUMBER = "device_serial_number" +DEVICE_REVISION = "device_revision" +DEVICE_INFO = "device_info" +DEVICE_MODEL = "device_model" +DEVICE_FIRMWARE = "device_firmware" + +SCENE_NAME = "name" +SCENE_ID = "id" +ROOM_ID_IN_SCENE = "roomId" + +SHADE_NAME = "name" +SHADE_ID = "id" +ROOM_ID_IN_SHADE = "roomId" + +ROOM_NAME = "name" +ROOM_NAME_UNICODE = "name_unicode" +ROOM_ID = "id" + +SHADE_RESPONSE = "shade" +SHADE_BATTERY_LEVEL = "batteryStrength" +SHADE_BATTERY_LEVEL_MAX = 200 + +STATE_ATTRIBUTE_ROOM_NAME = "roomName" + +PV_API = "pv_api" +PV_HUB = "pv_hub" +PV_SHADES = "pv_shades" +PV_SCENE_DATA = "pv_scene_data" +PV_SHADE_DATA = "pv_shade_data" +PV_ROOM_DATA = "pv_room_data" +COORDINATOR = "coordinator" + +HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py new file mode 100644 index 00000000000..e14142677e3 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -0,0 +1,277 @@ +"""Support for hunter douglas shades.""" +import asyncio +import logging + +from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA +from aiopvapi.resources.shade import ( + ATTR_POSKIND1, + MAX_POSITION, + MIN_POSITION, + factory as PvShade, +) +import async_timeout + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_SHADE, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DEVICE_MODEL, + DOMAIN, + PV_API, + PV_ROOM_DATA, + PV_SHADE_DATA, + ROOM_ID_IN_SHADE, + ROOM_NAME_UNICODE, + SHADE_RESPONSE, + STATE_ATTRIBUTE_ROOM_NAME, +) +from .entity import ShadeEntity + +_LOGGER = logging.getLogger(__name__) + +# Estimated time it takes to complete a transition +# from one state to another +TRANSITION_COMPLETE_DURATION = 30 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the hunter douglas shades.""" + + pv_data = hass.data[DOMAIN][entry.entry_id] + room_data = pv_data[PV_ROOM_DATA] + shade_data = pv_data[PV_SHADE_DATA] + pv_request = pv_data[PV_API] + coordinator = pv_data[COORDINATOR] + device_info = pv_data[DEVICE_INFO] + + entities = [] + for raw_shade in shade_data.values(): + # The shade may be out of sync with the hub + # so we force a refresh when we add it if + # possible + shade = PvShade(raw_shade, pv_request) + name_before_refresh = shade.name + try: + async with async_timeout.timeout(1): + await shade.refresh() + except asyncio.TimeoutError: + # Forced refresh is not required for setup + pass + entities.append( + PowerViewShade( + shade, name_before_refresh, room_data, coordinator, device_info + ) + ) + async_add_entities(entities) + + +def hd_position_to_hass(hd_position): + """Convert hunter douglas position to hass position.""" + return round((hd_position / MAX_POSITION) * 100) + + +def hass_position_to_hd(hass_positon): + """Convert hass position to hunter douglas position.""" + return int(hass_positon / 100 * MAX_POSITION) + + +class PowerViewShade(ShadeEntity, CoverEntity): + """Representation of a powerview shade.""" + + def __init__(self, shade, name, room_data, coordinator, device_info): + """Initialize the shade.""" + room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) + super().__init__(coordinator, device_info, shade, name) + self._shade = shade + self._device_info = device_info + self._is_opening = False + self._is_closing = False + self._last_action_timestamp = 0 + self._scheduled_transition_update = None + self._room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") + self._current_cover_position = MIN_POSITION + self._coordinator = coordinator + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name} + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + if self._device_info[DEVICE_MODEL] != "1": + supported_features |= SUPPORT_STOP + return supported_features + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._current_cover_position == MIN_POSITION + + @property + def is_opening(self): + """Return if the cover is opening.""" + return self._is_opening + + @property + def is_closing(self): + """Return if the cover is closing.""" + return self._is_closing + + @property + def current_cover_position(self): + """Return the current position of cover.""" + return hd_position_to_hass(self._current_cover_position) + + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_SHADE + + @property + def name(self): + """Return the name of the shade.""" + return self._shade_name + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + await self._async_move(0) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._async_move(100) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + # Cancel any previous updates + self._async_cancel_scheduled_transition_update() + self._async_update_from_command(await self._shade.stop()) + await self._async_force_refresh_state() + + async def async_set_cover_position(self, **kwargs): + """Move the shade to a specific position.""" + if ATTR_POSITION not in kwargs: + return + await self._async_move(kwargs[ATTR_POSITION]) + + async def _async_move(self, target_hass_position): + """Move the shade to a position.""" + current_hass_position = hd_position_to_hass(self._current_cover_position) + steps_to_move = abs(current_hass_position - target_hass_position) + if not steps_to_move: + return + self._async_schedule_update_for_transition(steps_to_move) + self._async_update_from_command( + await self._shade.move( + { + ATTR_POSITION1: hass_position_to_hd(target_hass_position), + ATTR_POSKIND1: 1, + } + ) + ) + self._is_opening = False + self._is_closing = False + if target_hass_position > current_hass_position: + self._is_opening = True + elif target_hass_position < current_hass_position: + self._is_closing = True + self.async_write_ha_state() + + @callback + def _async_update_from_command(self, raw_data): + """Update the shade state after a command.""" + if not raw_data or SHADE_RESPONSE not in raw_data: + return + self._async_process_new_shade_data(raw_data[SHADE_RESPONSE]) + + @callback + def _async_process_new_shade_data(self, data): + """Process new data from an update.""" + self._shade.raw_data = data + self._async_update_current_cover_position() + + @callback + def _async_update_current_cover_position(self): + """Update the current cover position from the data.""" + _LOGGER.debug("Raw data update: %s", self._shade.raw_data) + position_data = self._shade.raw_data[ATTR_POSITION_DATA] + if ATTR_POSITION1 in position_data: + self._current_cover_position = position_data[ATTR_POSITION1] + self._is_opening = False + self._is_closing = False + + @callback + def _async_cancel_scheduled_transition_update(self): + """Cancel any previous updates.""" + if not self._scheduled_transition_update: + return + self._scheduled_transition_update() + self._scheduled_transition_update = None + + @callback + def _async_schedule_update_for_transition(self, steps): + self.async_write_ha_state() + + # Cancel any previous updates + self._async_cancel_scheduled_transition_update() + + est_time_to_complete_transition = 1 + int( + TRANSITION_COMPLETE_DURATION * (steps / 100) + ) + + _LOGGER.debug( + "Estimated time to complete transition of %s steps for %s: %s", + steps, + self.name, + est_time_to_complete_transition, + ) + + # Schedule an update for when we expect the transition + # to be completed. + self._scheduled_transition_update = async_call_later( + self.hass, + est_time_to_complete_transition, + self._async_complete_schedule_update, + ) + + async def _async_complete_schedule_update(self, _): + """Update status of the cover.""" + _LOGGER.debug("Processing scheduled update for %s", self.name) + self._scheduled_transition_update = None + await self._async_force_refresh_state() + + async def _async_force_refresh_state(self): + """Refresh the cover state and force the device cache to be bypassed.""" + await self._shade.refresh() + self._async_update_current_cover_position() + self.async_write_ha_state() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self._async_update_current_cover_position() + self.async_on_remove( + self._coordinator.async_add_listener(self._async_update_shade_from_group) + ) + + @callback + def _async_update_shade_from_group(self): + """Update with new data from the coordinator.""" + if self._scheduled_transition_update: + # If a transition in in progress + # the data will be wrong + return + self._async_process_new_shade_data(self._coordinator.data[self._shade.id]) + self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py new file mode 100644 index 00000000000..3c98eeaf615 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -0,0 +1,92 @@ +"""The nexia integration base entity.""" + +from aiopvapi.resources.shade import ATTR_TYPE + +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import Entity + +from .const import ( + DEVICE_FIRMWARE, + DEVICE_MAC_ADDRESS, + DEVICE_MODEL, + DEVICE_NAME, + DEVICE_SERIAL_NUMBER, + DOMAIN, + FIRMWARE_BUILD, + FIRMWARE_IN_SHADE, + FIRMWARE_REVISION, + FIRMWARE_SUB_REVISION, + MANUFACTURER, +) + + +class HDEntity(Entity): + """Base class for hunter douglas entities.""" + + def __init__(self, coordinator, device_info, unique_id): + """Initialize the entity.""" + super().__init__() + self._coordinator = coordinator + self._unique_id = unique_id + self._device_info = device_info + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def device_info(self): + """Return the device_info of the device.""" + firmware = self._device_info[DEVICE_FIRMWARE] + sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" + return { + "identifiers": {(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])}, + "connections": { + (dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS]) + }, + "name": self._device_info[DEVICE_NAME], + "model": self._device_info[DEVICE_MODEL], + "sw_version": sw_version, + "manufacturer": MANUFACTURER, + } + + +class ShadeEntity(HDEntity): + """Base class for hunter douglas shade entities.""" + + def __init__(self, coordinator, device_info, shade, shade_name): + """Initialize the shade.""" + super().__init__(coordinator, device_info, shade.id) + self._shade_name = shade_name + self._shade = shade + + @property + def device_info(self): + """Return the device_info of the device.""" + firmware = self._shade.raw_data[FIRMWARE_IN_SHADE] + sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" + model = self._shade.raw_data[ATTR_TYPE] + for shade in self._shade.shade_types: + if shade.shade_type == model: + model = shade.description + break + + return { + "identifiers": {(DOMAIN, self._shade.id)}, + "name": self._shade_name, + "model": str(model), + "sw_version": sw_version, + "manufacturer": MANUFACTURER, + "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), + } diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 68fc6118a34..b68ec02d3f6 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,6 +2,12 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==1.6.14"], - "codeowners": [] -} + "requirements": [ + "aiopvapi==1.6.14" + ], + "codeowners": ["@bdraco"], + "config_flow": true, + "homekit": { + "models": ["PowerView"] + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index b73ce8fd7d5..89e723a02ac 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -2,86 +2,73 @@ import logging from typing import Any -from aiopvapi.helpers.aiorequest import AioRequest from aiopvapi.resources.scene import Scene as PvScene -from aiopvapi.rooms import Rooms -from aiopvapi.scenes import Scenes import voluptuous as vol -from homeassistant.components.scene import DOMAIN, Scene -from homeassistant.const import CONF_PLATFORM -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.components.scene import Scene +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_HOST, CONF_PLATFORM import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import async_generate_entity_id + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DOMAIN, + HUB_ADDRESS, + PV_API, + PV_ROOM_DATA, + PV_SCENE_DATA, + ROOM_NAME_UNICODE, + STATE_ATTRIBUTE_ROOM_NAME, +) +from .entity import HDEntity _LOGGER = logging.getLogger(__name__) -ENTITY_ID_FORMAT = DOMAIN + ".{}" -HUB_ADDRESS = "address" PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): "hunterdouglas_powerview", - vol.Required(HUB_ADDRESS): cv.string, - } + {vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(HUB_ADDRESS): cv.string} ) -SCENE_DATA = "sceneData" -ROOM_DATA = "roomData" -SCENE_NAME = "name" -ROOM_NAME = "name" -SCENE_ID = "id" -ROOM_ID = "id" -ROOM_ID_IN_SCENE = "roomId" -STATE_ATTRIBUTE_ROOM_NAME = "roomName" - - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Home Assistant scene entries.""" + """Import platform from yaml.""" - hub_address = config.get(HUB_ADDRESS) - websession = async_get_clientsession(hass) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: config[HUB_ADDRESS]}, + ) + ) - pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) - _scenes = await Scenes(pv_request).get_resources() - _rooms = await Rooms(pv_request).get_resources() +async def async_setup_entry(hass, entry, async_add_entities): + """Set up powerview scene entries.""" + + pv_data = hass.data[DOMAIN][entry.entry_id] + room_data = pv_data[PV_ROOM_DATA] + scene_data = pv_data[PV_SCENE_DATA] + pv_request = pv_data[PV_API] + coordinator = pv_data[COORDINATOR] + device_info = pv_data[DEVICE_INFO] - if not _scenes or not _rooms: - _LOGGER.error("Unable to initialize PowerView hub: %s", hub_address) - return pvscenes = ( - PowerViewScene(hass, PvScene(_raw_scene, pv_request), _rooms) - for _raw_scene in _scenes[SCENE_DATA] + PowerViewScene( + PvScene(raw_scene, pv_request), room_data, coordinator, device_info + ) + for scene_id, raw_scene in scene_data.items() ) async_add_entities(pvscenes) -class PowerViewScene(Scene): +class PowerViewScene(HDEntity, Scene): """Representation of a Powerview scene.""" - def __init__(self, hass, scene, room_data): + def __init__(self, scene, room_data, coordinator, device_info): """Initialize the scene.""" + super().__init__(coordinator, device_info, scene.id) self._scene = scene - self.hass = hass - self._room_name = None - self._sync_room_data(room_data) - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, str(self._scene.id), hass=hass - ) - - def _sync_room_data(self, room_data): - """Sync room data.""" - room = next( - ( - room - for room in room_data[ROOM_DATA] - if room[ROOM_ID] == self._scene.room_id - ), - {}, - ) - - self._room_name = room.get(ROOM_NAME, "") + self._room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "") @property def name(self): diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py new file mode 100644 index 00000000000..794fdac3eac --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -0,0 +1,86 @@ +"""Support for hunterdouglass_powerview sensors.""" +import logging + +from aiopvapi.resources.shade import factory as PvShade + +from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.core import callback + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DOMAIN, + PV_API, + PV_SHADE_DATA, + SHADE_BATTERY_LEVEL, + SHADE_BATTERY_LEVEL_MAX, +) +from .entity import ShadeEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the hunter douglas shades sensors.""" + + pv_data = hass.data[DOMAIN][entry.entry_id] + shade_data = pv_data[PV_SHADE_DATA] + pv_request = pv_data[PV_API] + coordinator = pv_data[COORDINATOR] + device_info = pv_data[DEVICE_INFO] + + entities = [] + for raw_shade in shade_data.values(): + shade = PvShade(raw_shade, pv_request) + if SHADE_BATTERY_LEVEL not in shade.raw_data: + continue + name_before_refresh = shade.name + entities.append( + PowerViewShadeBatterySensor( + coordinator, device_info, shade, name_before_refresh + ) + ) + async_add_entities(entities) + + +class PowerViewShadeBatterySensor(ShadeEntity): + """Representation of an shade battery charge sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return UNIT_PERCENTAGE + + @property + def name(self): + """Name of the shade battery.""" + return f"{self._shade_name} Battery" + + @property + def device_class(self): + """Shade battery Class.""" + return DEVICE_CLASS_BATTERY + + @property + def unique_id(self): + """Shade battery Uniqueid.""" + return f"{self._unique_id}_charge" + + @property + def state(self): + """Get the current value in percentage.""" + return round( + self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100 + ) + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self._async_update_shade_from_group) + ) + + @callback + def _async_update_shade_from_group(self): + """Update with new data from the coordinator.""" + self._shade.raw_data = self._coordinator.data[self._shade.id] + self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/strings.json b/homeassistant/components/hunterdouglas_powerview/strings.json new file mode 100644 index 00000000000..4cba22b60fb --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/strings.json @@ -0,0 +1,24 @@ +{ + "title": "Hunter Douglas PowerView", + "config": { + "step": { + "user": { + "title": "Connect to the PowerView Hub", + "data": { + "host": "[%key:common::config_flow::data::ip%]" + } + }, + "link": { + "title": "Connect to the PowerView Hub", + "description": "Do you want to setup {name} ({host})?" + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ca.json b/homeassistant/components/hunterdouglas_powerview/translations/ca.json new file mode 100644 index 00000000000..de30d18bfff --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "unknown": "Error inesperat" + }, + "step": { + "link": { + "description": "Vols configurar {name} ({host})?", + "title": "Connexi\u00f3 amb el Hub PowerView" + }, + "user": { + "data": { + "host": "Adre\u00e7a IP" + }, + "title": "Connexi\u00f3 amb el Hub PowerView" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/de.json b/homeassistant/components/hunterdouglas_powerview/translations/de.json new file mode 100644 index 00000000000..50dc39b3d98 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "link": { + "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "title": "Stellen Sie eine Verbindung zum PowerView Hub her" + }, + "user": { + "data": { + "host": "IP-Adresse" + }, + "title": "Stellen Sie eine Verbindung zum PowerView Hub her" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/en.json b/homeassistant/components/hunterdouglas_powerview/translations/en.json new file mode 100644 index 00000000000..b4574e6473f --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "step": { + "link": { + "description": "Do you want to setup {name} ({host})?", + "title": "Connect to the PowerView Hub" + }, + "user": { + "data": { + "host": "IP address" + }, + "title": "Connect to the PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es-419.json b/homeassistant/components/hunterdouglas_powerview/translations/es-419.json new file mode 100644 index 00000000000..c4a26d7d35d --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "link": { + "description": "\u00bfDesea configurar {name} ({host})?", + "title": "Conectar a Powerview Hub" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP" + }, + "title": "Conectar a Powerview Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json new file mode 100644 index 00000000000..46e216976bd --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "unknown": "Error inesperado" + }, + "step": { + "link": { + "description": "\u00bfQuieres configurar {name} ({host})?", + "title": "Conectar con el PowerView Hub" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP" + }, + "title": "Conectar con el PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fi.json b/homeassistant/components/hunterdouglas_powerview/translations/fi.json new file mode 100644 index 00000000000..3dfa8931bff --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/fi.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty" + }, + "error": { + "cannot_connect": "Yhteyden muodostaminen ep\u00e4onnistui. Yrit\u00e4 uudelleen", + "unknown": "Odottamaton virhe" + }, + "step": { + "link": { + "title": "Yhdist\u00e4 PowerView-keskittimeen" + }, + "user": { + "data": { + "host": "IP-osoite" + }, + "title": "Yhdist\u00e4 PowerView-keskittimeen" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json new file mode 100644 index 00000000000..a1bd06078c6 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "step": { + "link": { + "description": "Voulez-vous configurer {name} ({host})?", + "title": "Connectez-vous au concentrateur PowerView" + }, + "user": { + "data": { + "host": "Adresse IP" + }, + "title": "Connectez-vous au concentrateur PowerView" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/he.json b/homeassistant/components/hunterdouglas_powerview/translations/he.json new file mode 100644 index 00000000000..39beabfa06e --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json new file mode 100644 index 00000000000..baa3b135d42 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "IP-c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/it.json b/homeassistant/components/hunterdouglas_powerview/translations/it.json new file mode 100644 index 00000000000..2dcb2a72c8b --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "step": { + "link": { + "description": "Vuoi impostare {name} ({host})?", + "title": "Connettersi all'Hub PowerView" + }, + "user": { + "data": { + "host": "Indirizzo IP" + }, + "title": "Collegamento al PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ko.json b/homeassistant/components/hunterdouglas_powerview/translations/ko.json new file mode 100644 index 00000000000..23a02dd2a5f --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "link": { + "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "PowerView \ud5c8\ube0c\uc5d0 \uc5f0\uacb0\ud558\uae30" + }, + "user": { + "data": { + "host": "IP \uc8fc\uc18c" + }, + "title": "PowerView \ud5c8\ube0c\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/lb.json b/homeassistant/components/hunterdouglas_powerview/translations/lb.json new file mode 100644 index 00000000000..a32c15e57d1 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "link": { + "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?", + "title": "Mam PowerView Hub verbannen" + }, + "user": { + "data": { + "host": "IP Adresse" + }, + "title": "Mam PowerView Hub verbannen" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/nl.json b/homeassistant/components/hunterdouglas_powerview/translations/nl.json new file mode 100644 index 00000000000..9c0ab4932de --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "link": { + "description": "Wil je {name} ({host}) instellen?", + "title": "Maak verbinding met de PowerView Hub" + }, + "user": { + "data": { + "host": "IP-adres" + }, + "title": "Maak verbinding met de PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/no.json b/homeassistant/components/hunterdouglas_powerview/translations/no.json new file mode 100644 index 00000000000..836ecb64446 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "step": { + "link": { + "description": "Vil du konfigurere {name} ({host})?", + "title": "Koble til PowerView-huben" + }, + "user": { + "data": { + "host": "IP adresse" + }, + "title": "Koble til PowerView-huben" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json new file mode 100644 index 00000000000..774007fc86e --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "unknown": "[%key_id:common::config_flow::error::unknown%]" + }, + "step": { + "link": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Po\u0142\u0105cz si\u0119 z hubem PowerView" + }, + "user": { + "data": { + "host": "Adres IP" + }, + "title": "Po\u0142\u0105cz si\u0119 z hubem PowerView" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ru.json b/homeassistant/components/hunterdouglas_powerview/translations/ru.json new file mode 100644 index 00000000000..88d363c63da --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/ru.json @@ -0,0 +1,24 @@ +{ + "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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "link": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", + "title": "Hunter Douglas PowerView" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "title": "Hunter Douglas PowerView" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sl.json b/homeassistant/components/hunterdouglas_powerview/translations/sl.json new file mode 100644 index 00000000000..4edc104f3fc --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "link": { + "description": "Ali \u017eelite nastaviti {name} ({host})?", + "title": "Pove\u017eite se s PowerView Hub" + }, + "user": { + "data": { + "host": "IP naslov" + }, + "title": "Pove\u017eite se s PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sv.json b/homeassistant/components/hunterdouglas_powerview/translations/sv.json new file mode 100644 index 00000000000..04371b16514 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "link": { + "description": "Do vill du konfigurera {name} ({host})?", + "title": "Anslut till PowerView Hub" + }, + "user": { + "data": { + "host": "IP-adress" + }, + "title": "Anslut till PowerView Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json new file mode 100644 index 00000000000..0eb5a4d3d37 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "link": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", + "title": "\u9023\u7dda\u81f3 PowerView Hub" + }, + "user": { + "data": { + "host": "IP \u4f4d\u5740" + }, + "title": "\u9023\u7dda\u81f3 PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 037c48b029e..389506c6d5a 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice): +class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): """A sensor implementation for Hydrawise device.""" @property diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index aa3780060c3..577fde85d37 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -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_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class HydrawiseSwitch(HydrawiseEntity, SwitchDevice): +class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): """A switch implementation for Hydrawise device.""" def __init__(self, default_watering_timer, *args): diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index fc96f672afb..d1baec315bf 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([device]) -class Hyperion(Light): +class Hyperion(LightEntity): """Representation of a Hyperion remote.""" def __init__( diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 24ab2bc7a80..67c9ec891a6 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ialarm], True) -class IAlarmPanel(alarm.AlarmControlPanel): +class IAlarmPanel(alarm.AlarmControlPanelEntity): """Representation of an iAlarm status.""" def __init__(self, name, code, username, password, url): diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 30d419c1bce..669188c473f 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_COLD, DOMAIN, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -27,7 +27,7 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorDevice): +class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorEntity): """Representation of a binary sensor.""" @property diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 36f3303774a..2c26b2bc363 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -10,7 +10,7 @@ from iaqualink.const import ( AQUALINK_TEMP_FAHRENHEIT_LOW, ) -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( DOMAIN, HVAC_MODE_HEAT, @@ -39,7 +39,7 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkThermostat(AqualinkEntity, ClimateDevice): +class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): """Representation of a thermostat.""" @property diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 813af7863f1..362ef8b8b8e 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -32,7 +32,7 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkLight(AqualinkEntity, Light): +class HassAqualinkLight(AqualinkEntity, LightEntity): """Representation of a light.""" @property diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json index f4ad099be83..a861fd35420 100644 --- a/homeassistant/components/iaqualink/strings.json +++ b/homeassistant/components/iaqualink/strings.json @@ -5,8 +5,8 @@ "title": "Connect to iAqualink", "description": "Please enter the username and password for your iAqualink account.", "data": { - "username": "Username / Email Address", - "password": "Password" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -17,4 +17,4 @@ "already_setup": "You can only configure a single iAqualink connection." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index 8efb473cf54..b71253ade12 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,7 +1,7 @@ """Support for Aqualink pool feature switches.""" import logging -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -23,7 +23,7 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkSwitch(AqualinkEntity, SwitchDevice): +class HassAqualinkSwitch(AqualinkEntity, SwitchEntity): """Representation of a switch.""" @property diff --git a/homeassistant/components/iaqualink/translations/en.json b/homeassistant/components/iaqualink/translations/en.json index bebad01c319..8c66ad88e6b 100644 --- a/homeassistant/components/iaqualink/translations/en.json +++ b/homeassistant/components/iaqualink/translations/en.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "Username / Email Address" + "username": "Username" }, "description": "Please enter the username and password for your iAqualink account.", "title": "Connect to iAqualink" diff --git a/homeassistant/components/iaqualink/translations/es-419.json b/homeassistant/components/iaqualink/translations/es-419.json index 170c2851d08..9d43bfa7e57 100644 --- a/homeassistant/components/iaqualink/translations/es-419.json +++ b/homeassistant/components/iaqualink/translations/es-419.json @@ -1,12 +1,19 @@ { "config": { + "abort": { + "already_setup": "Solo puede configurar una \u00fanica conexi\u00f3n iAqualink." + }, + "error": { + "connection_failure": "No se puede conectar a iAqualink. Verifice su nombre de usuario y contrase\u00f1a." + }, "step": { "user": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario / direcci\u00f3n de correo electr\u00f3nico" }, - "description": "Por favor, Ingrese el nombre de usuario y la contrase\u00f1a para su cuenta de iAqualink." + "description": "Por favor, Ingrese el nombre de usuario y la contrase\u00f1a para su cuenta de iAqualink.", + "title": "Conectar a iAqualink" } } } diff --git a/homeassistant/components/iaqualink/translations/ko.json b/homeassistant/components/iaqualink/translations/ko.json index 3399960dd33..179a862e67d 100644 --- a/homeassistant/components/iaqualink/translations/ko.json +++ b/homeassistant/components/iaqualink/translations/ko.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 / \uc774\uba54\uc77c \uc8fc\uc18c" + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "iAqualink \uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/iaqualink/translations/no.json b/homeassistant/components/iaqualink/translations/no.json index 622cf931cba..b6f6d65d062 100644 --- a/homeassistant/components/iaqualink/translations/no.json +++ b/homeassistant/components/iaqualink/translations/no.json @@ -12,7 +12,7 @@ "password": "Passord", "username": "Brukernavn / E-postadresse" }, - "description": "Vennligst skriv inn brukernavn og passord for iAqualink-kontoen din.", + "description": "Vennligst fyll inn brukernavn og passord for iAqualink-kontoen din.", "title": "Koble til iAqualink" } } diff --git a/homeassistant/components/iaqualink/translations/pl.json b/homeassistant/components/iaqualink/translations/pl.json index dd8ca0adc30..a247cadf1a4 100644 --- a/homeassistant/components/iaqualink/translations/pl.json +++ b/homeassistant/components/iaqualink/translations/pl.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika/adres e-mail" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]/adres e-mail" }, "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.", "title": "Po\u0142\u0105cz z iAqualink" diff --git a/homeassistant/components/iaqualink/translations/ru.json b/homeassistant/components/iaqualink/translations/ru.json index a5d75662546..14c12efcf47 100644 --- a/homeassistant/components/iaqualink/translations/ru.json +++ b/homeassistant/components/iaqualink/translations/ru.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", "title": "Jandy iAqualink" diff --git a/homeassistant/components/iaqualink/translations/zh-Hant.json b/homeassistant/components/iaqualink/translations/zh-Hant.json index 7161fec645f..99d5f434380 100644 --- a/homeassistant/components/iaqualink/translations/zh-Hant.json +++ b/homeassistant/components/iaqualink/translations/zh-Hant.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31 / \u96fb\u5b50\u90f5\u4ef6" + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8acb\u8f38\u5165 iAqualink \u5e33\u865f\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002", "title": "\u9023\u7dda\u81f3 iAqualink" diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index b9e22a7b042..7153bb9340e 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -5,20 +5,24 @@ "title": "iCloud credentials", "description": "Enter your credentials", "data": { - "username": "Email", - "password": "Password", + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", "with_family": "With family" } }, "trusted_device": { "title": "iCloud trusted device", "description": "Select your trusted device", - "data": { "trusted_device": "Trusted device" } + "data": { + "trusted_device": "Trusted device" + } }, "verification_code": { "title": "iCloud verification code", "description": "Please enter the verification code you just received from iCloud", - "data": { "verification_code": "Verification code" } + "data": { + "verification_code": "Verification code" + } } }, "error": { @@ -31,4 +35,4 @@ "no_device": "None of your devices have \"Find my iPhone\" activated" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index ade0c4a3bd3..5fa4ee6626d 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -5,7 +5,7 @@ "no_device": "Cap dels teus dispositius t\u00e9 activada la opci\u00f3 \"Troba el meu iPhone\"" }, "error": { - "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", + "login": "Error d'inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s" }, diff --git a/homeassistant/components/icloud/translations/da.json b/homeassistant/components/icloud/translations/da.json index 1c179b25322..a06950ca30d 100644 --- a/homeassistant/components/icloud/translations/da.json +++ b/homeassistant/components/icloud/translations/da.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email", "with_family": "Med familien" }, "description": "Indtast dine legitimationsoplysninger", diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 0b06ae422d0..d09889453e0 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail", "with_family": "Mit Familie" }, "description": "Gib deine Zugangsdaten ein", diff --git a/homeassistant/components/icloud/translations/es-419.json b/homeassistant/components/icloud/translations/es-419.json new file mode 100644 index 00000000000..250235eea62 --- /dev/null +++ b/homeassistant/components/icloud/translations/es-419.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "no_device": "Ninguno de sus dispositivos tiene activado \"Buscar mi iPhone\"" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su correo electr\u00f3nico y contrase\u00f1a", + "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "validate_verification_code": "No se pudo verificar su c\u00f3digo de verificaci\u00f3n, elija un dispositivo de confianza y comience la verificaci\u00f3n nuevamente" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo de confianza" + }, + "description": "Selecciona tu dispositivo de confianza", + "title": "Dispositivo de confianza iCloud" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "with_family": "Con la familia" + }, + "description": "Ingrese sus credenciales", + "title": "Credenciales de iCloud" + }, + "verification_code": { + "data": { + "verification_code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Ingrese el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "title": "C\u00f3digo de verificaci\u00f3n de iCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 49bd9d612eb..c2cbaf175f9 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Correo electr\u00f3nico", "with_family": "Con la familia" }, "description": "Ingrese sus credenciales", diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index cce095e0298..61aacd004ea 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -20,7 +20,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "with_family": "Avec la famille" }, "description": "Entrez vos identifiants", "title": "Identifiants iCloud" diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json new file mode 100644 index 00000000000..71466dddc39 --- /dev/null +++ b/homeassistant/components/icloud/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 4dcf547619c..194b92eb2fc 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -15,8 +15,7 @@ }, "user": { "data": { - "password": "Jelsz\u00f3", - "username": "E-mail" + "password": "Jelsz\u00f3" }, "description": "Adja meg hiteles\u00edt\u0151 adatait", "title": "iCloud hiteles\u00edt\u0151 adatok" diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 27429081e1e..80cdfed7ace 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Password", - "username": "E-mail", "with_family": "Con la famiglia" }, "description": "Inserisci le tue credenziali", diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index f5c3be53639..c27c96e570a 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -10,8 +10,7 @@ }, "user": { "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "E\u30e1\u30fc\u30eb" + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" diff --git a/homeassistant/components/icloud/translations/lb.json b/homeassistant/components/icloud/translations/lb.json index b6e9abe94bc..9e923d6ffd1 100644 --- a/homeassistant/components/icloud/translations/lb.json +++ b/homeassistant/components/icloud/translations/lb.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Passwuert", - "username": "E-Mail", "with_family": "Mat der Famill" }, "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus", diff --git a/homeassistant/components/icloud/translations/lv.json b/homeassistant/components/icloud/translations/lv.json index bbfb1fe32b1..817d4bdddf8 100644 --- a/homeassistant/components/icloud/translations/lv.json +++ b/homeassistant/components/icloud/translations/lv.json @@ -8,8 +8,7 @@ }, "user": { "data": { - "password": "Parole", - "username": "E-pasts" + "password": "Parole" } } } diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 84691ebd134..1088b7bb16e 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account reeds geconfigureerd" + "already_configured": "Account reeds geconfigureerd", + "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd" }, "error": { "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", @@ -19,7 +20,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "E-mail" + "with_family": "Met gezin" }, "description": "Voer uw gegevens in", "title": "iCloud inloggegevens" diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index f1384df73b9..021ebd1d71b 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -20,17 +20,16 @@ "user": { "data": { "password": "Passord", - "username": "E-post", "with_family": "Med familie" }, - "description": "Angi legitimasjonsbeskrivelsen", + "description": "Fyll inn legitimasjonen din", "title": "iCloud-legitimasjon" }, "verification_code": { "data": { "verification_code": "iCloud-bekreftelseskode" }, - "description": "Vennligst skriv inn bekreftelseskoden du nettopp har f\u00e5tt fra iCloud", + "description": "Vennligst fyll inn bekreftelseskoden du nettopp har mottatt fra iCloud", "title": "iCloud-bekreftelseskode" } } diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json index 20e3f8c2fb4..0bccf920554 100644 --- a/homeassistant/components/icloud/translations/pl.json +++ b/homeassistant/components/icloud/translations/pl.json @@ -19,8 +19,8 @@ }, "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%]", "with_family": "Z rodzin\u0105" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", diff --git a/homeassistant/components/icloud/translations/pt-BR.json b/homeassistant/components/icloud/translations/pt-BR.json index 53a0e0090dd..364c0aca85c 100644 --- a/homeassistant/components/icloud/translations/pt-BR.json +++ b/homeassistant/components/icloud/translations/pt-BR.json @@ -18,8 +18,7 @@ }, "user": { "data": { - "password": "Senha", - "username": "Email" + "password": "Senha" }, "description": "Insira suas credenciais", "title": "credenciais do iCloud" diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index 420196bb050..ec4e06338ba 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -8,8 +8,7 @@ }, "user": { "data": { - "password": "Palavra-passe", - "username": "Email" + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/icloud/translations/sl.json b/homeassistant/components/icloud/translations/sl.json index 18ffb070948..9e259bb0516 100644 --- a/homeassistant/components/icloud/translations/sl.json +++ b/homeassistant/components/icloud/translations/sl.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Geslo", - "username": "E-po\u0161tni naslov", "with_family": "Z dru\u017eino" }, "description": "Vnesite svoje poverilnice", diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index 3ca8252a558..9f11709fcb4 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -18,8 +18,7 @@ }, "user": { "data": { - "password": "L\u00f6senord", - "username": "E-post" + "password": "L\u00f6senord" }, "description": "Ange dina autentiseringsuppgifter", "title": "iCloud-autentiseringsuppgifter" diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 2c281e58c48..783cd16fefe 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -8,7 +8,7 @@ from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, FORMAT_TEXT, PLATFORM_SCHEMA, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -108,7 +108,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class IFTTTAlarmPanel(AlarmControlPanel): +class IFTTTAlarmPanel(AlarmControlPanelEntity): """Representation of an alarm control panel controlled through IFTTT.""" def __init__( diff --git a/homeassistant/components/ifttt/translations/es-419.json b/homeassistant/components/ifttt/translations/es-419.json index 097ecad5b79..4b648d64279 100644 --- a/homeassistant/components/ifttt/translations/es-419.json +++ b/homeassistant/components/ifttt/translations/es-419.json @@ -9,7 +9,8 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?" + "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?", + "title": "Configurar el Applet de webhook IFTTT" } } } diff --git a/homeassistant/components/ifttt/translations/fi.json b/homeassistant/components/ifttt/translations/fi.json new file mode 100644 index 00000000000..0570e4a1a66 --- /dev/null +++ b/homeassistant/components/ifttt/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Vain yksi instanssi on tarpeen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/ko.json b/homeassistant/components/ifttt/translations/ko.json index 30dd8b04c11..f3c0cb1062c 100644 --- a/homeassistant/components/ifttt/translations/ko.json +++ b/homeassistant/components/ifttt/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "description": "IFTTT \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "IFTTT Webhook \uc560\ud50c\ub9bf \uc124\uc815" + "title": "IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index 59e6db2a81f..f6f681d8b60 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([IGloLamp(name, host, port)], True) -class IGloLamp(Light): +class IGloLamp(LightEntity): """Representation of an iGlo light.""" def __init__(self, name, host, port): diff --git a/homeassistant/components/ihc/binary_sensor.py b/homeassistant/components/ihc/binary_sensor.py index 3f59d7981fb..0c6a685bc93 100644 --- a/homeassistant/components/ihc/binary_sensor.py +++ b/homeassistant/components/ihc/binary_sensor.py @@ -1,5 +1,5 @@ """Support for IHC binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_TYPE from . import IHC_CONTROLLER, IHC_INFO @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class IHCBinarySensor(IHCDevice, BinarySensorDevice): +class IHCBinarySensor(IHCDevice, BinarySensorEntity): """IHC Binary Sensor. The associated IHC resource can be any in or output from a IHC product diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index af6b62c42ff..c35cb2224cf 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,7 +1,11 @@ """Support for IHC lights.""" import logging -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from . import IHC_CONTROLLER, IHC_INFO from .const import CONF_DIMMABLE, CONF_OFF_ID, CONF_ON_ID @@ -35,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class IhcLight(IHCDevice, Light): +class IhcLight(IHCDevice, LightEntity): """Representation of a IHC light. For dimmable lights, the associated IHC resource should be a light diff --git a/homeassistant/components/ihc/switch.py b/homeassistant/components/ihc/switch.py index 15994f13eb2..8f72ac3bab8 100644 --- a/homeassistant/components/ihc/switch.py +++ b/homeassistant/components/ihc/switch.py @@ -1,5 +1,5 @@ """Support for IHC switches.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import IHC_CONTROLLER, IHC_INFO from .const import CONF_OFF_ID, CONF_ON_ID @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class IHCSwitch(IHCDevice, SwitchDevice): +class IHCSwitch(IHCDevice, SwitchEntity): """Representation of an IHC switch.""" def __init__( diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index a97d2a1d02b..ea93c2bb975 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -230,6 +230,8 @@ class EmailContentSensor(Entity): email_message = self._email_reader.read_next() if email_message is None: + self._message = None + self._state_attributes = {} return if self.sender_allowed(email_message): diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index f15c2298b9d..bf1340fb235 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Optional from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDevice, + BinarySensorEntity, ) from . import DOMAIN, IncomfortChild @@ -20,7 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IncomfortFailed(client, h) for h in heaters]) -class IncomfortFailed(IncomfortChild, BinarySensorDevice): +class IncomfortFailed(IncomfortChild, BinarySensorEntity): """Representation of an InComfort Failed sensor.""" def __init__(self, client, heater) -> None: diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 464ff989941..274308efe06 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,7 +1,7 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, List, Optional -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateDevice +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, @@ -24,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class InComfortClimate(IncomfortChild, ClimateDevice): +class InComfortClimate(IncomfortChild, ClimateEntity): """Representation of an InComfort/InTouch climate device.""" def __init__(self, client, heater, room) -> None: diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 88370acf166..da6e6d89315 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -7,7 +7,7 @@ from aiohttp import ClientResponseError from homeassistant.components.water_heater import ( DOMAIN as WATER_HEATER_DOMAIN, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IncomfortWaterHeater(client, h) for h in heaters]) -class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): +class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): """Representation of an InComfort/Intouch water_heater device.""" def __init__(self, client, heater) -> None: diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 922a0197cf1..0d1999e0d7b 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_SSL, CONF_USERNAME, @@ -83,6 +84,7 @@ CONFIG_SCHEMA = vol.Schema( } ), vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, + vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL): cv.boolean, vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, @@ -131,6 +133,9 @@ def setup(hass, config): if CONF_HOST in conf: kwargs["host"] = conf[CONF_HOST] + if CONF_PATH in conf: + kwargs["path"] = conf[CONF_PATH] + if CONF_PORT in conf: kwargs["port"] = conf[CONF_PORT] diff --git a/homeassistant/components/input_boolean/translations/no.json b/homeassistant/components/input_boolean/translations/no.json index b6ffe9f30ef..b0a608a1754 100644 --- a/homeassistant/components/input_boolean/translations/no.json +++ b/homeassistant/components/input_boolean/translations/no.json @@ -1,3 +1,9 @@ { - "title": "Valgt boolsk" + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Inndata boolsk" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/no.json b/homeassistant/components/input_datetime/translations/no.json index d86f7180c81..e9a36c0fc88 100644 --- a/homeassistant/components/input_datetime/translations/no.json +++ b/homeassistant/components/input_datetime/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Angi dato" + "title": "Inndata datotid" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/no.json b/homeassistant/components/input_number/translations/no.json index c36d2b6aafc..cc918fabb2f 100644 --- a/homeassistant/components/input_number/translations/no.json +++ b/homeassistant/components/input_number/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Angi nummer" + "title": "Inndata nummer" } \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/es.json b/homeassistant/components/input_select/translations/es.json index ad5d4266d2c..2ff356dc725 100644 --- a/homeassistant/components/input_select/translations/es.json +++ b/homeassistant/components/input_select/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Selecci\u00f3n de entrada" + "title": "Entrada de selecci\u00f3n" } \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/no.json b/homeassistant/components/input_select/translations/no.json index 6840f1416c8..c5802730c42 100644 --- a/homeassistant/components/input_select/translations/no.json +++ b/homeassistant/components/input_select/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Angi valg" + "title": "Inndata valg" } \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/no.json b/homeassistant/components/input_text/translations/no.json index a643fa15fda..bf41f9dc43c 100644 --- a/homeassistant/components/input_text/translations/no.json +++ b/homeassistant/components/input_text/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Angi tekst" + "title": "Inndata tekst" } \ No newline at end of file diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index b96feb21831..81c3c58ef12 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -1,7 +1,7 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from .insteon_entity import InsteonEntity @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([new_entity]) -class InsteonBinarySensor(InsteonEntity, BinarySensorDevice): +class InsteonBinarySensor(InsteonEntity, BinarySensorEntity): """A Class for an Insteon device entity.""" def __init__(self, device, state_key): diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index 575799cbf67..b325a6ebd84 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -7,7 +7,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, - CoverDevice, + CoverEntity, ) from .insteon_entity import InsteonEntity @@ -34,12 +34,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device.states[state_key].name, ) - new_entity = InsteonCoverDevice(device, state_key) + new_entity = InsteonCoverEntity(device, state_key) async_add_entities([new_entity]) -class InsteonCoverDevice(InsteonEntity, CoverDevice): +class InsteonCoverEntity(InsteonEntity, CoverEntity): """A Class for an Insteon device.""" @property diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 60a27b3acb8..afd575c363b 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,7 +1,11 @@ """Support for Insteon lights via PowerLinc Modem.""" import logging -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from .insteon_entity import InsteonEntity @@ -29,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([new_entity]) -class InsteonDimmerDevice(InsteonEntity, Light): +class InsteonDimmerDevice(InsteonEntity, LightEntity): """A Class for an Insteon device.""" @property diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index c88d31d2f91..3a0668459c9 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,7 +1,7 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from .insteon_entity import InsteonEntity @@ -32,7 +32,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([new_entity]) -class InsteonSwitchDevice(InsteonEntity, SwitchDevice): +class InsteonSwitchDevice(InsteonEntity, SwitchEntity): """A Class for an Insteon device.""" @property @@ -49,7 +49,7 @@ class InsteonSwitchDevice(InsteonEntity, SwitchDevice): self._insteon_device_state.off() -class InsteonOpenClosedDevice(InsteonEntity, SwitchDevice): +class InsteonOpenClosedDevice(InsteonEntity, SwitchEntity): """A Class for an Insteon device.""" @property diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index a3a06a52c9c..ecd00bde986 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -5,7 +5,7 @@ from random import randrange from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, HVAC_MODE_COOL, @@ -129,7 +129,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await controller.stop() -class IntesisAC(ClimateDevice): +class IntesisAC(ClimateEntity): """Represents an Intesishome air conditioning device.""" def __init__(self, ih_device_id, ih_device, controller): diff --git a/homeassistant/components/ios/translations/fi.json b/homeassistant/components/ios/translations/fi.json new file mode 100644 index 00000000000..f88bd919e33 --- /dev/null +++ b/homeassistant/components/ios/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Home Assistant iOS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/no.json b/homeassistant/components/ios/translations/no.json index 5645bd91e2a..2814d73f555 100644 --- a/homeassistant/components/ios/translations/no.json +++ b/homeassistant/components/ios/translations/no.json @@ -5,8 +5,8 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 konfigurere Home Assistant iOS-komponenten?", - "title": "Home Assistant iOS" + "description": "\u00d8nsker du \u00e5 sette opp Home Assistant iOS-komponenten?", + "title": "" } } } diff --git a/homeassistant/components/ipma/translations/es-419.json b/homeassistant/components/ipma/translations/es-419.json index 7b319c66a49..a3f83c150e7 100644 --- a/homeassistant/components/ipma/translations/es-419.json +++ b/homeassistant/components/ipma/translations/es-419.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitud", "longitude": "Longitud", + "mode": "Modo", "name": "Nombre" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/translations/fi.json b/homeassistant/components/ipma/translations/fi.json new file mode 100644 index 00000000000..00ae9b3df2f --- /dev/null +++ b/homeassistant/components/ipma/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "name_exists": "Nimi on jo olemassa" + }, + "step": { + "user": { + "data": { + "latitude": "Leveysaste", + "longitude": "Pituusaste", + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 7d1c3d1b1b8..3128583f218 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -152,6 +152,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.debug( "Unable to determine unique id from discovery info and IPP response" ) + return self.async_abort(reason="unique_id_required") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured( diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index e3e28ace1fd..09c2424151f 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -6,29 +6,30 @@ "title": "Link your printer", "description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.", "data": { - "host": "Host or IP address", - "port": "Port", + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", "base_path": "Relative path to the printer", "ssl": "Printer supports communication over SSL/TLS", "verify_ssl": "Printer uses a proper SSL certificate" } }, "zeroconf_confirm": { - "description": "Do you want to add the printer named `{name}` to Home Assistant?", + "description": "Do you want to set up {name}?", "title": "Discovered printer" } }, "error": { - "connection_error": "Failed to connect to printer.", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked." }, "abort": { - "already_configured": "This printer is already configured.", - "connection_error": "Failed to connect to printer.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", "ipp_error": "Encountered IPP error.", "ipp_version_error": "IPP version not supported by printer.", - "parse_error": "Failed to parse response from printer." + "parse_error": "Failed to parse response from printer.", + "unique_id_required": "Device missing unique identification required for discovery." } } } diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index 8ed12b70d6a..a7bc8d04890 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Aquesta impressora ja est\u00e0 configurada.", - "connection_error": "No s'ha pogut connectar amb la impressora.", + "already_configured": "El dispositiu ja est\u00e0 configurat", + "connection_error": "No s'ha pogut connectar", "connection_upgrade": "No s'ha pogut connectar amb la impressora, es necessita actualitzar la connexi\u00f3.", "ipp_error": "S'ha produ\u00eft un error IPP.", "ipp_version_error": "La versi\u00f3 IPP no \u00e9s compatible amb la impressora.", "parse_error": "No s'ha pogut analitzar la resposta de la impressora." }, "error": { - "connection_error": "No s'ha pogut connectar amb la impressora.", + "connection_error": "No s'ha pogut connectar", "connection_upgrade": "No s'ha pogut connectar amb la impressora. Prova-ho novament amb l'opci\u00f3 SSL/TLS activada." }, "flow_title": "Impressora: {name}", @@ -17,7 +17,7 @@ "user": { "data": { "base_path": "Ruta relativa a la impressora", - "host": "Amfitri\u00f3 o adre\u00e7a IP", + "host": "Amfitri\u00f3", "port": "Port", "ssl": "La impressora \u00e9s compatible amb comunicaci\u00f3 SSL/TLS", "verify_ssl": "La impressora utilitza un certificat SSL adequat" diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json index 7fe5132ed64..abbe4e8a5f8 100644 --- a/homeassistant/components/ipp/translations/en.json +++ b/homeassistant/components/ipp/translations/en.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "This printer is already configured.", - "connection_error": "Failed to connect to printer.", + "already_configured": "Device is already configured", + "connection_error": "Failed to connect", "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", "ipp_error": "Encountered IPP error.", "ipp_version_error": "IPP version not supported by printer.", "parse_error": "Failed to parse response from printer." }, "error": { - "connection_error": "Failed to connect to printer.", + "connection_error": "Failed to connect", "connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked." }, "flow_title": "Printer: {name}", @@ -17,7 +17,7 @@ "user": { "data": { "base_path": "Relative path to the printer", - "host": "Host or IP address", + "host": "Host", "port": "Port", "ssl": "Printer supports communication over SSL/TLS", "verify_ssl": "Printer uses a proper SSL certificate" @@ -26,7 +26,7 @@ "title": "Link your printer" }, "zeroconf_confirm": { - "description": "Do you want to add the printer named `{name}` to Home Assistant?", + "description": "Do you want to set up {name}?", "title": "Discovered printer" } } diff --git a/homeassistant/components/ipp/translations/es-419.json b/homeassistant/components/ipp/translations/es-419.json new file mode 100644 index 00000000000..eb9c21eb28c --- /dev/null +++ b/homeassistant/components/ipp/translations/es-419.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "connection_error": "No se pudo conectar a la impresora.", + "connection_upgrade": "No se pudo conectar a la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", + "ipp_error": "Error de IPP encontrado.", + "ipp_version_error": "La versi\u00f3n IPP no es compatible con la impresora.", + "parse_error": "Error al analizar la respuesta de la impresora." + }, + "error": { + "connection_error": "No se pudo conectar a la impresora.", + "connection_upgrade": "No se pudo conectar a la impresora. Intente nuevamente con la opci\u00f3n SSL/TLS marcada." + }, + "flow_title": "Impresora: {name}", + "step": { + "user": { + "data": { + "base_path": "Ruta relativa a la impresora", + "host": "Host o direcci\u00f3n IP", + "port": "Puerto", + "ssl": "La impresora admite la comunicaci\u00f3n a trav\u00e9s de SSL/TLS", + "verify_ssl": "La impresora usa un certificado SSL adecuado" + }, + "description": "Configure su impresora a trav\u00e9s del Protocolo de impresi\u00f3n de Internet (IPP) para integrarse con Home Assistant.", + "title": "Enlace su impresora" + }, + "zeroconf_confirm": { + "description": "\u00bfDesea agregar la impresora llamada `{name}` a Home Assistant?", + "title": "Impresora descubierta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index bdb55b539fa..5f4a1370b68 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Esta impresora ya est\u00e1 configurada.", - "connection_error": "No se pudo conectar con la impresora.", + "already_configured": "Dispositivo ya configurado", + "connection_error": "Error al conectar", "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", "ipp_error": "Error IPP encontrado.", "ipp_version_error": "Versi\u00f3n de IPP no compatible con la impresora.", "parse_error": "Error al analizar la respuesta de la impresora." }, "error": { - "connection_error": "No se pudo conectar con la impresora.", + "connection_error": "Error al conectar", "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." }, "flow_title": "Impresora: {name}", @@ -26,7 +26,7 @@ "title": "Vincula tu impresora" }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir la impresora llamada `{name}` a Home Assistant?", + "description": "\u00bfQuieres configurar {name}?", "title": "Impresora encontrada" } } diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json new file mode 100644 index 00000000000..66e835ec100 --- /dev/null +++ b/homeassistant/components/ipp/translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Nyomtat\u00f3: {name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ko.json b/homeassistant/components/ipp/translations/ko.json index a0f79d0417f..abf0c270dac 100644 --- a/homeassistant/components/ipp/translations/ko.json +++ b/homeassistant/components/ipp/translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\uc774 \ud504\ub9b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "connection_error": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc5f0\uacb0\uc744 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4.", "ipp_error": "IPP \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "ipp_version_error": "\ud504\ub9b0\ud130\uc5d0\uc11c IPP \ubc84\uc804\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "parse_error": "\ud504\ub9b0\ud130\uc758 \uc751\ub2f5\uc744 \uc77d\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." }, "error": { - "connection_error": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "flow_title": "\ud504\ub9b0\ud130: {name}", @@ -17,16 +17,16 @@ "user": { "data": { "base_path": "\ud504\ub9b0\ud130\uc758 \uc0c1\ub300 \uacbd\ub85c", - "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8", "ssl": "\ud504\ub9b0\ud130\ub294 SSL/TLS \ub97c \ud1b5\ud55c \ud1b5\uc2e0\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4", "verify_ssl": "\ud504\ub9b0\ud130\ub294 \uc62c\ubc14\ub978 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" }, "description": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP) \ub97c \ud1b5\ud574 \ud504\ub9b0\ud130\ub97c \uc124\uc815\ud558\uc5ec Home Assistant \uc640 \uc5f0\ub3d9\ud569\ub2c8\ub2e4.", - "title": "\ud504\ub9b0\ud130 \uc5f0\uacb0" + "title": "\ud504\ub9b0\ud130 \uc5f0\uacb0\ud558\uae30" }, "zeroconf_confirm": { - "description": "Home Assistant \uc5d0 `{name}` \ud504\ub9b0\ud130\ub97c \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ubc1c\uacac\ub41c \ud504\ub9b0\ud130" } } diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index d6fbeb4cbbd..12f9d37ec7a 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -1,3 +1,34 @@ { - "title": "Internet Printing Protocol (IPP)" + "config": { + "abort": { + "already_configured": "Deze printer is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met de printer.", + "connection_upgrade": "Kan geen verbinding maken met de printer omdat een upgrade van de verbinding vereist is.", + "ipp_error": "Er is een IPP-fout opgetreden.", + "ipp_version_error": "IPP-versie wordt niet ondersteund door printer.", + "parse_error": "Ongeldige reactie van de printer." + }, + "error": { + "connection_error": "Kan geen verbinding maken met de printer.", + "connection_upgrade": "Kan geen verbinding maken met de printer. Probeer het opnieuw met SSL / TLS-optie aangevinkt." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relatief pad naar de printer", + "host": "Host- of IP-adres", + "port": "Poort", + "ssl": "Printer ondersteunt communicatie via SSL / TLS", + "verify_ssl": "Printer gebruikt een correct SSL-certificaat" + }, + "description": "Stel uw printer in via Internet Printing Protocol (IPP) om te integreren met Home Assistant.", + "title": "Koppel uw printer" + }, + "zeroconf_confirm": { + "description": "Wilt u de printer met de naam ' {name} ' toevoegen aan Home Assistant?", + "title": "Gedetecteerde printer" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index 2d4fe3e64b4..afc03cf90b2 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -22,7 +22,7 @@ "ssl": "Skriveren st\u00f8tter kommunikasjon over SSL/TLS", "verify_ssl": "Skriveren bruker et riktig SSL-sertifikat" }, - "description": "Konfigurer skriveren din via Internet Printing Protocol (IPP) for \u00e5 integrere med Home Assistant.", + "description": "Sett opp skriveren din via Internet Printing Protocol (IPP) for \u00e5 integrere med Home Assistant.", "title": "Koble til skriveren din" }, "zeroconf_confirm": { diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index 8bc57941cb6..8ead666dee1 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Ta drukarka jest ju\u017c skonfigurowana.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z drukark\u0105.", "connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105 z powodu konieczno\u015bci uaktualnienia po\u0142\u0105czenia.", "ipp_error": "Wyst\u0105pi\u0142 b\u0142\u0105d IPP.", - "ipp_version_error": "Wersja IPP nie obs\u0142ugiwana przez drukark\u0119.", + "ipp_version_error": "Wersja IPP nieobs\u0142ugiwana przez drukark\u0119.", "parse_error": "Nie mo\u017cna przeanalizowa\u0107 odpowiedzi z drukarki." }, "error": { @@ -17,8 +17,8 @@ "user": { "data": { "base_path": "\u015acie\u017cka wzgl\u0119dna do drukarki", - "host": "Nazwa hosta lub adres IP", - "port": "Port", + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]", "ssl": "Drukarka obs\u0142uguje komunikacj\u0119 przez SSL/TLS", "verify_ssl": "Drukarka u\u017cywa prawid\u0142owego certyfikatu" }, diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index 5a8c6068769..c2d9e6b5cc0 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443.", + "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.", + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f.", "ipp_error": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 IPP.", "ipp_version_error": "\u0412\u0435\u0440\u0441\u0438\u044f IPP \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c.", "parse_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430." }, "error": { - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443.", + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL/TLS." }, "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", @@ -17,12 +17,12 @@ "user": { "data": { "base_path": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443", - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u044f\u0437\u044c \u043f\u043e SSL/TLS", "verify_ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \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 SSL" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP.", "title": "Internet Printing Protocol (IPP)" }, "zeroconf_confirm": { diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index 68cda398da2..fa7593c1ea2 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "connection_error": "\u5370\u8868\u6a5f\u9023\u7dda\u5931\u6557\u3002", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "connection_error": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002", "ipp_error": "\u767c\u751f IPP \u932f\u8aa4\u3002", "ipp_version_error": "\u4e0d\u652f\u63f4\u5370\u8868\u6a5f\u7684 IPP \u7248\u672c\u3002", "parse_error": "\u7372\u5f97\u5370\u8868\u6a5f\u56de\u61c9\u5931\u6557\u3002" }, "error": { - "connection_error": "\u5370\u8868\u6a5f\u9023\u7dda\u5931\u6557\u3002", + "connection_error": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002\u8acb\u52fe\u9078 SSL/TLS \u9078\u9805\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" }, "flow_title": "\u5370\u8868\u6a5f\uff1a{name}", @@ -17,7 +17,7 @@ "user": { "data": { "base_path": "\u5370\u8868\u6a5f\u76f8\u5c0d\u8def\u5f91", - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", "ssl": "\u5370\u8868\u6a5f\u652f\u63f4 SSL/TLS \u901a\u8a0a", "verify_ssl": "\u5370\u8868\u6a5f\u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49" @@ -26,7 +26,7 @@ "title": "\u9023\u7d50\u5370\u8868\u6a5f" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u65b0\u589e\u540d\u7a31 `{name}` \u5370\u8868\u6a5f\u81f3 Home Assistant\uff1f", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684\u5370\u8868\u6a5f" } } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 154cdd43c65..0acbecddf8d 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.18.2", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.18.4", "pyiqvia==0.2.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/iqvia/translations/es-419.json b/homeassistant/components/iqvia/translations/es-419.json index b107e1bb696..21b5273bfba 100644 --- a/homeassistant/components/iqvia/translations/es-419.json +++ b/homeassistant/components/iqvia/translations/es-419.json @@ -1,13 +1,16 @@ { "config": { "error": { + "identifier_exists": "C\u00f3digo postal ya registrado", "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" }, "step": { "user": { "data": { "zip_code": "C\u00f3digo postal" - } + }, + "description": "Complete su c\u00f3digo postal de EE. UU. o Canad\u00e1.", + "title": "IQVIA" } } } diff --git a/homeassistant/components/iqvia/translations/fi.json b/homeassistant/components/iqvia/translations/fi.json new file mode 100644 index 00000000000..151e9755a6a --- /dev/null +++ b/homeassistant/components/iqvia/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Postinumero on jo rekister\u00f6ity", + "invalid_zip_code": "Postinumero on virheellinen" + }, + "step": { + "user": { + "data": { + "zip_code": "Postinumero" + }, + "title": "IQVIA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/es-419.json b/homeassistant/components/islamic_prayer_times/translations/es-419.json new file mode 100644 index 00000000000..2a5105a4054 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "step": { + "user": { + "description": "\u00bfDesea establecer tiempos de oraci\u00f3n isl\u00e1mica?", + "title": "Establecer tiempos de oraci\u00f3n isl\u00e1mica" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "M\u00e9todo de c\u00e1lculo de la oraci\u00f3n" + } + } + } + }, + "title": "Tiempos de oraci\u00f3n isl\u00e1mica" +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/fr.json b/homeassistant/components/islamic_prayer_times/translations/fr.json new file mode 100644 index 00000000000..6499df244ab --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "step": { + "user": { + "description": "Voulez-vous configurer Islamic Prayer Times?", + "title": "Configurer Islamic Prayer Times" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "M\u00e9thode de calcul de la pri\u00e8re" + } + } + } + }, + "title": "Islamic Prayer Times" +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/it.json b/homeassistant/components/islamic_prayer_times/translations/it.json index 02f3c5df2ec..ff1d085a58d 100644 --- a/homeassistant/components/islamic_prayer_times/translations/it.json +++ b/homeassistant/components/islamic_prayer_times/translations/it.json @@ -5,8 +5,8 @@ }, "step": { "user": { - "description": "Vuoi impostare i tempi di preghiera islamici?", - "title": "Impostare i tempi di preghiera islamici" + "description": "Vuoi impostare gli Orari di Preghiera Islamici?", + "title": "Impostare gli Orari di Preghiera Islamici" } } }, @@ -19,5 +19,5 @@ } } }, - "title": "Tempi di preghiera islamica" + "title": "Orari di Preghiera Islamici" } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/ko.json b/homeassistant/components/islamic_prayer_times/translations/ko.json new file mode 100644 index 00000000000..300e4661795 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "user": { + "description": "\uc774\uc2ac\ub78c \uae30\ub3c4 \uc2dc\uac04\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc774\uc2ac\ub78c \uae30\ub3c4 \uc2dc\uac04 \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "\uc774\uc2ac\ub78c \uae30\ub3c4 \uc2dc\uac04 \uacc4\uc0b0 \ubc29\ubc95" + } + } + } + }, + "title": "\uc774\uc2ac\ub78c \uae30\ub3c4 \uc2dc\uac04" +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/ru.json b/homeassistant/components/islamic_prayer_times/translations/ru.json index 799c4bc5500..66f2e918f65 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ru.json +++ b/homeassistant/components/islamic_prayer_times/translations/ru.json @@ -14,7 +14,7 @@ "step": { "init": { "data": { - "calculation_method": "\u041c\u0435\u0442\u043e\u0434 \u0440\u0430\u0441\u0447\u0435\u0442\u0430" + "calculation_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0440\u0430\u0441\u0447\u0435\u0442\u0430" } } } diff --git a/homeassistant/components/islamic_prayer_times/translations/sl.json b/homeassistant/components/islamic_prayer_times/translations/sl.json new file mode 100644 index 00000000000..6cef11fccac --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "step": { + "user": { + "description": "Ali \u017eelite nastaviti Islamski \u010das molitve?", + "title": "Nastavite islamske molitvene \u010dase" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Na\u010din izra\u010duna molitve" + } + } + } + }, + "title": "Islamski molitveni \u010dasi" +} \ No newline at end of file diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 3b8e222c912..e1f0d7a19ce 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -6,7 +6,7 @@ import pyiss import requests import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([IssBinarySensor(iss_data, name, show_on_map)], True) -class IssBinarySensor(BinarySensorDevice): +class IssBinarySensor(BinarySensorEntity): """Implementation of the ISS binary sensor.""" def __init__(self, iss_data, name, show): diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index f0766c4e4f9..ffeb6079e5d 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,40 +1,42 @@ """Support the ISY-994 controllers.""" -from collections import namedtuple -import logging +import asyncio +from functools import partial +from typing import Optional from urllib.parse import urlparse -import PyISY -from PyISY.Nodes import Group +from pyisy import ISY import voluptuous as vol -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, - UNIT_PERCENTAGE, +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from .const import ( + _LOGGER, + CONF_IGNORE_STRING, + CONF_RESTORE_LIGHT_STATE, + CONF_SENSOR_STRING, + CONF_TLS_VER, + CONF_VAR_SENSOR_STRING, + DEFAULT_IGNORE_STRING, + DEFAULT_RESTORE_LIGHT_STATE, + DEFAULT_SENSOR_STRING, + DEFAULT_VAR_SENSOR_STRING, + DOMAIN, + ISY994_ISY, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_VARIABLES, + MANUFACTURER, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, + UNDO_UPDATE_LISTENER, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, Dict - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "isy994" - -CONF_IGNORE_STRING = "ignore_string" -CONF_SENSOR_STRING = "sensor_string" -CONF_ENABLE_CLIMATE = "enable_climate" -CONF_TLS_VER = "tls" - -DEFAULT_IGNORE_STRING = "{IGNORE ME}" -DEFAULT_SENSOR_STRING = "sensor" - -KEY_ACTIONS = "actions" -KEY_FOLDER = "folder" -KEY_MY_PROGRAMS = "My Programs" -KEY_STATUS = "status" +from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables +from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( { @@ -50,370 +52,89 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional( CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING ): cv.string, - vol.Optional(CONF_ENABLE_CLIMATE, default=True): cv.boolean, + vol.Optional( + CONF_VAR_SENSOR_STRING, default=DEFAULT_VAR_SENSOR_STRING + ): cv.string, + vol.Required( + CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE + ): bool, } ) }, extra=vol.ALLOW_EXTRA, ) -# Do not use the Home Assistant consts for the states here - we're matching -# exact API responses, not using them for Home Assistant states -NODE_FILTERS = { - "binary_sensor": { - "uom": [], - "states": [], - "node_def_id": ["BinaryAlarm", "BinaryAlarm_ADV"], - "insteon_type": ["16."], # Does a startswith() match; include the dot - }, - "sensor": { - # This is just a more-readable way of including MOST uoms between 1-100 - # (Remember that range() is non-inclusive of the stop value) - "uom": ( - ["1"] - + list(map(str, range(3, 11))) - + list(map(str, range(12, 51))) - + list(map(str, range(52, 66))) - + list(map(str, range(69, 78))) - + ["79"] - + list(map(str, range(82, 97))) - ), - "states": [], - "node_def_id": ["IMETER_SOLO"], - "insteon_type": ["9.0.", "9.7."], - }, - "lock": { - "uom": ["11"], - "states": ["locked", "unlocked"], - "node_def_id": ["DoorLock"], - "insteon_type": ["15."], - }, - "fan": { - "uom": [], - "states": ["off", "low", "med", "high"], - "node_def_id": ["FanLincMotor"], - "insteon_type": ["1.46."], - }, - "cover": { - "uom": ["97"], - "states": ["open", "closed", "closing", "opening", "stopped"], - "node_def_id": [], - "insteon_type": [], - }, - "light": { - "uom": ["51"], - "states": ["on", "off", UNIT_PERCENTAGE], - "node_def_id": [ - "DimmerLampSwitch", - "DimmerLampSwitch_ADV", - "DimmerSwitchOnly", - "DimmerSwitchOnly_ADV", - "DimmerLampOnly", - "BallastRelayLampSwitch", - "BallastRelayLampSwitch_ADV", - "RemoteLinc2", - "RemoteLinc2_ADV", - "KeypadDimmer", - "KeypadDimmer_ADV", - ], - "insteon_type": ["1."], - }, - "switch": { - "uom": ["2", "78"], - "states": ["on", "off"], - "node_def_id": [ - "OnOffControl", - "RelayLampSwitch", - "RelayLampSwitch_ADV", - "RelaySwitchOnlyPlusQuery", - "RelaySwitchOnlyPlusQuery_ADV", - "RelayLampOnly", - "RelayLampOnly_ADV", - "KeypadButton", - "KeypadButton_ADV", - "EZRAIN_Input", - "EZRAIN_Output", - "EZIO2x4_Input", - "EZIO2x4_Input_ADV", - "BinaryControl", - "BinaryControl_ADV", - "AlertModuleSiren", - "AlertModuleSiren_ADV", - "AlertModuleArmed", - "Siren", - "Siren_ADV", - "X10", - "KeypadRelay", - "KeypadRelay_ADV", - ], - "insteon_type": ["2.", "9.10.", "9.11.", "113."], - }, -} -SUPPORTED_DOMAINS = [ - "binary_sensor", - "sensor", - "lock", - "fan", - "cover", - "light", - "switch", -] -SUPPORTED_PROGRAM_DOMAINS = ["binary_sensor", "lock", "fan", "cover", "switch"] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the isy994 integration from YAML.""" + isy_config: Optional[ConfigType] = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) -# ISY Scenes are more like Switches than Home Assistant Scenes -# (they can turn off, and report their state) -SCENE_DOMAIN = "switch" - -ISY994_NODES = "isy994_nodes" -ISY994_WEATHER = "isy994_weather" -ISY994_PROGRAMS = "isy994_programs" - -WeatherNode = namedtuple("WeatherNode", ("status", "name", "uom")) - - -def _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> bool: - """Check if the node matches the node_def_id for any domains. - - This is only present on the 5.0 ISY firmware, and is the most reliable - way to determine a device's type. - """ - if not hasattr(node, "node_def_id") or node.node_def_id is None: - # Node doesn't have a node_def (pre 5.0 firmware most likely) - return False - - node_def_id = node.node_def_id - - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_def_id in NODE_FILTERS[domain]["node_def_id"]: - hass.data[ISY994_NODES][domain].append(node) - return True - - _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) - return False - - -def _check_for_insteon_type( - hass: HomeAssistant, node, single_domain: str = None -) -> bool: - """Check if the node matches the Insteon type for any domains. - - This is for (presumably) every version of the ISY firmware, but only - works for Insteon device. "Node Server" (v5+) and Z-Wave and others will - not have a type. - """ - if not hasattr(node, "type") or node.type is None: - # Node doesn't have a type (non-Insteon device most likely) - return False - - device_type = node.type - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if any( - [ - device_type.startswith(t) - for t in set(NODE_FILTERS[domain]["insteon_type"]) - ] - ): - - # Hacky special-case just for FanLinc, which has a light module - # as one of its nodes. Note that this special-case is not necessary - # on ISY 5.x firmware as it uses the superior NodeDefs method - if domain == "fan" and int(node.nid[-1]) == 1: - hass.data[ISY994_NODES]["light"].append(node) - return True - - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_uom_id( - hass: HomeAssistant, node, single_domain: str = None, uom_list: list = None -) -> bool: - """Check if a node's uom matches any of the domains uom filter. - - This is used for versions of the ISY firmware that report uoms as a single - ID. We can often infer what type of device it is by that ID. - """ - if not hasattr(node, "uom") or node.uom is None: - # Node doesn't have a uom (Scenes for example) - return False - - node_uom = set(map(str.lower, node.uom)) - - if uom_list: - if node_uom.intersection(uom_list): - hass.data[ISY994_NODES][single_domain].append(node) - return True - else: - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_uom.intersection(NODE_FILTERS[domain]["uom"]): - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_states_in_uom( - hass: HomeAssistant, node, single_domain: str = None, states_list: list = None -) -> bool: - """Check if a list of uoms matches two possible filters. - - This is for versions of the ISY firmware that report uoms as a list of all - possible "human readable" states. This filter passes if all of the possible - states fit inside the given filter. - """ - if not hasattr(node, "uom") or node.uom is None: - # Node doesn't have a uom (Scenes for example) - return False - - node_uom = set(map(str.lower, node.uom)) - - if states_list: - if node_uom == set(states_list): - hass.data[ISY994_NODES][single_domain].append(node) - return True - else: - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_uom == set(NODE_FILTERS[domain]["states"]): - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: - """Determine if the given sensor node should be a binary_sensor.""" - if _check_for_node_def(hass, node, single_domain="binary_sensor"): - return True - if _check_for_insteon_type(hass, node, single_domain="binary_sensor"): + if not isy_config: return True - # For the next two checks, we're providing our own set of uoms that - # represent on/off devices. This is because we can only depend on these - # checks in the context of already knowing that this is definitely a - # sensor device. - if _check_for_uom_id( - hass, node, single_domain="binary_sensor", uom_list=["2", "78"] - ): - return True - if _check_for_states_in_uom( - hass, node, single_domain="binary_sensor", states_list=["on", "off"] - ): - return True - - return False - - -def _categorize_nodes( - hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str -) -> None: - """Sort the nodes to their proper domains.""" - for (path, node) in nodes: - ignored = ignore_identifier in path or ignore_identifier in node.name - if ignored: - # Don't import this node as a device at all - continue - - if isinstance(node, Group): - hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) - continue - - if sensor_identifier in path or sensor_identifier in node.name: - # User has specified to treat this as a sensor. First we need to - # determine if it should be a binary_sensor. - if _is_sensor_a_binary_sensor(hass, node): - continue - - hass.data[ISY994_NODES]["sensor"].append(node) - continue - - # We have a bunch of different methods for determining the device type, - # each of which works with different ISY firmware versions or device - # family. The order here is important, from most reliable to least. - if _check_for_node_def(hass, node): - continue - if _check_for_insteon_type(hass, node): - continue - if _check_for_uom_id(hass, node): - continue - if _check_for_states_in_uom(hass, node): - continue - - -def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: - """Categorize the ISY994 programs.""" - for domain in SUPPORTED_PROGRAM_DOMAINS: - try: - folder = programs[KEY_MY_PROGRAMS][f"HA.{domain}"] - except KeyError: - pass - else: - for dtype, _, node_id in folder.children: - if dtype != KEY_FOLDER: - continue - entity_folder = folder[node_id] - try: - status = entity_folder[KEY_STATUS] - assert status.dtype == "program", "Not a program" - if domain != "binary_sensor": - actions = entity_folder[KEY_ACTIONS] - assert actions.dtype == "program", "Not a program" - else: - actions = None - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning( - "Program entity '%s' not loaded due " - "to invalid folder structure.", - entity_folder.name, - ) - continue - - entity = (entity_folder.name, status, actions) - hass.data[ISY994_PROGRAMS][domain].append(entity) - - -def _categorize_weather(hass: HomeAssistant, climate) -> None: - """Categorize the ISY994 weather data.""" - climate_attrs = dir(climate) - weather_nodes = [ - WeatherNode( - getattr(climate, attr), - attr.replace("_", " "), - getattr(climate, f"{attr}_units"), + # Only import if we haven't before. + config_entry = _async_find_matching_config_entry(hass) + if not config_entry: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=dict(isy_config), + ) ) - for attr in climate_attrs - if f"{attr}_units" in climate_attrs - ] - hass.data[ISY994_WEATHER].extend(weather_nodes) + return True + + # Update the entry based on the YAML configuration, in case it changed. + hass.config_entries.async_update_entry(config_entry, data=dict(isy_config)) + return True -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the ISY 994 platform.""" - hass.data[ISY994_NODES] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_NODES][domain] = [] +@callback +def _async_find_matching_config_entry(hass): + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.source == config_entries.SOURCE_IMPORT: + return entry - hass.data[ISY994_WEATHER] = [] - hass.data[ISY994_PROGRAMS] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_PROGRAMS][domain] = [] +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up the ISY 994 integration.""" + # As there currently is no way to import options from yaml + # when setting up a config entry, we fallback to adding + # the options to the config entry and pull them out here if + # they are missing from the options + _async_import_options_from_data_if_missing(hass, entry) - isy_config = config.get(DOMAIN) + hass.data[DOMAIN][entry.entry_id] = {} + hass_isy_data = hass.data[DOMAIN][entry.entry_id] - user = isy_config.get(CONF_USERNAME) - password = isy_config.get(CONF_PASSWORD) + hass_isy_data[ISY994_NODES] = {} + for platform in SUPPORTED_PLATFORMS: + hass_isy_data[ISY994_NODES][platform] = [] + + hass_isy_data[ISY994_PROGRAMS] = {} + for platform in SUPPORTED_PROGRAM_PLATFORMS: + hass_isy_data[ISY994_PROGRAMS][platform] = [] + + hass_isy_data[ISY994_VARIABLES] = [] + + isy_config = entry.data + isy_options = entry.options + + # Required + user = isy_config[CONF_USERNAME] + password = isy_config[CONF_PASSWORD] + host = urlparse(isy_config[CONF_HOST]) + + # Optional tls_version = isy_config.get(CONF_TLS_VER) - host = urlparse(isy_config.get(CONF_HOST)) - ignore_identifier = isy_config.get(CONF_IGNORE_STRING) - sensor_identifier = isy_config.get(CONF_SENSOR_STRING) - enable_climate = isy_config.get(CONF_ENABLE_CLIMATE) + ignore_identifier = isy_options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING) + sensor_identifier = isy_options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING) + variable_identifier = isy_options.get( + CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING + ) if host.scheme == "http": https = False @@ -426,112 +147,126 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return False # Connect to ISY controller. - isy = PyISY.ISY( - host.hostname, - port, - username=user, - password=password, - use_https=https, - tls_ver=tls_version, - log=_LOGGER, + isy = await hass.async_add_executor_job( + partial( + ISY, + host.hostname, + port, + username=user, + password=password, + use_https=https, + tls_ver=tls_version, + log=_LOGGER, + webroot=host.path, + ) ) if not isy.connected: return False - _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) - _categorize_programs(hass, isy.programs) + _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) + _categorize_programs(hass_isy_data, isy.programs) + _categorize_variables(hass_isy_data, isy.variables, variable_identifier) - if enable_climate and isy.configuration.get("Weather Information"): - _categorize_weather(hass, isy.climate) + # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs + _LOGGER.info(repr(isy.clock)) - def stop(event: object) -> None: - """Stop ISY auto updates.""" - isy.auto_update = False - - # Listen for HA stop to disconnect. - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) + hass_isy_data[ISY994_ISY] = isy + await _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. - for component in SUPPORTED_DOMAINS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in SUPPORTED_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + def _start_auto_update() -> None: + """Start isy auto update.""" + _LOGGER.debug("ISY Starting Event Stream and automatic updates.") + isy.auto_update = True + + await hass.async_add_executor_job(_start_auto_update) + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass_isy_data[UNDO_UPDATE_LISTENER] = undo_listener + + # Register Integration-wide Services: + async_setup_services(hass) - isy.auto_update = True return True -class ISYDevice(Entity): - """Representation of an ISY994 device.""" +async def _async_update_listener( + hass: HomeAssistant, entry: config_entries.ConfigEntry +): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) - _attrs = {} - _name: str = None - def __init__(self, node) -> None: - """Initialize the insteon device.""" - self._node = node - self._change_handler = None - self._control_handler = None +@callback +def _async_import_options_from_data_if_missing( + hass: HomeAssistant, entry: config_entries.ConfigEntry +): + options = dict(entry.options) + modified = False + for importable_option in [ + CONF_IGNORE_STRING, + CONF_SENSOR_STRING, + CONF_RESTORE_LIGHT_STATE, + ]: + if importable_option not in entry.options and importable_option in entry.data: + options[importable_option] = entry.data[importable_option] + modified = True - async def async_added_to_hass(self) -> None: - """Subscribe to the node change events.""" - self._change_handler = self._node.status.subscribe("changed", self.on_update) + if modified: + hass.config_entries.async_update_entry(entry, options=options) - if hasattr(self._node, "controlEvents"): - self._control_handler = self._node.controlEvents.subscribe(self.on_control) - def on_update(self, event: object) -> None: - """Handle the update event from the ISY994 Node.""" - self.schedule_update_ha_state() +async def _async_get_or_create_isy_device_in_registry( + hass: HomeAssistant, entry: config_entries.ConfigEntry, isy +) -> None: + device_registry = await dr.async_get_registry(hass) - def on_control(self, event: object) -> None: - """Handle a control event from the ISY994 Node.""" - self.hass.bus.fire( - "isy994_control", {"entity_id": self.entity_id, "control": event} + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration["uuid"])}, + identifiers={(DOMAIN, isy.configuration["uuid"])}, + manufacturer=MANUFACTURER, + name=isy.configuration["name"], + model=isy.configuration["model"], + sw_version=isy.configuration["firmware"], + ) + + +async def async_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in SUPPORTED_PLATFORMS + ] ) + ) - @property - def unique_id(self) -> str: - """Get the unique identifier of the device.""" - # pylint: disable=protected-access - if hasattr(self._node, "_id"): - return self._node._id + hass_isy_data = hass.data[DOMAIN][entry.entry_id] - return None + isy = hass_isy_data[ISY994_ISY] - @property - def name(self) -> str: - """Get the name of the device.""" - return self._name or str(self._node.name) + def _stop_auto_update() -> None: + """Start isy auto update.""" + _LOGGER.debug("ISY Stopping Event Stream and automatic updates.") + isy.auto_update = False - @property - def should_poll(self) -> bool: - """No polling required since we're using the subscription.""" - return False + await hass.async_add_executor_job(_stop_auto_update) - @property - def value(self) -> int: - """Get the current value of the device.""" - # pylint: disable=protected-access - return self._node.status._val + hass_isy_data[UNDO_UPDATE_LISTENER]() - def is_unknown(self) -> bool: - """Get whether or not the value of this Entity's node is unknown. + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - PyISY reports unknown values as -inf - """ - return self.value == -1 * float("inf") + async_unload_services(hass) - @property - def state(self): - """Return the state of the ISY device.""" - if self.is_unknown(): - return None - return super().state - - @property - def device_state_attributes(self) -> Dict: - """Get the state attributes for the device.""" - attr = {} - if hasattr(self._node, "aux_properties"): - for name, val in self._node.aux_properties.items(): - attr[name] = f"{val.get('value')} {val.get('uom')}" - return attr + return unload_ok diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 30b26ea5d24..3b5de4b8eca 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,130 +1,263 @@ """Support for ISY994 binary sensors.""" from datetime import timedelta -import logging -from typing import Callable +from typing import Callable, Union -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from homeassistant.const import STATE_OFF, STATE_ON +from pyisy.constants import ( + CMD_OFF, + CMD_ON, + ISY_VALUE_UNKNOWN, + PROTO_INSTEON, + PROTO_ZWAVE, +) +from pyisy.nodes import Group, Node + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, + DOMAIN as BINARY_SENSOR, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice +from .const import ( + _LOGGER, + BINARY_SENSOR_DEVICE_TYPES_ISY, + BINARY_SENSOR_DEVICE_TYPES_ZWAVE, + DOMAIN as ISY994_DOMAIN, + ISY994_NODES, + ISY994_PROGRAMS, + SUBNODE_CLIMATE_COOL, + SUBNODE_CLIMATE_HEAT, + SUBNODE_DUSK_DAWN, + SUBNODE_HEARTBEAT, + SUBNODE_LOW_BATTERY, + SUBNODE_MOTION_DISABLED, + SUBNODE_NEGATIVE, + SUBNODE_TAMPER, + TYPE_CATEGORY_CLIMATE, + TYPE_INSTEON_MOTION, +) +from .entity import ISYNodeEntity, ISYProgramEntity +from .helpers import migrate_old_unique_ids +from .services import async_setup_device_services -_LOGGER = logging.getLogger(__name__) - -ISY_DEVICE_TYPES = { - "moisture": ["16.8", "16.13", "16.14"], - "opening": ["16.9", "16.6", "16.7", "16.2", "16.17", "16.20", "16.21"], - "motion": ["16.1", "16.4", "16.5", "16.3"], -} +DEVICE_PARENT_REQUIRED = [ + DEVICE_CLASS_OPENING, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, +] -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 binary sensor platform.""" devices = [] - devices_by_nid = {} + devices_by_address = {} child_nodes = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - if node.parent_node is None: - device = ISYBinarySensorDevice(node) - devices.append(device) - devices_by_nid[node.nid] = device + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]: + device_class, device_type = _detect_device_type_and_class(node) + if node.protocol == PROTO_INSTEON: + if node.parent_node is not None: + # We'll process the Insteon child nodes last, to ensure all parent + # nodes have been processed + child_nodes.append((node, device_class, device_type)) + continue + device = ISYInsteonBinarySensorEntity(node, device_class) else: - # We'll process the child nodes last, to ensure all parent nodes - # have been processed - child_nodes.append(node) + device = ISYBinarySensorEntity(node, device_class) + devices.append(device) + devices_by_address[node.address] = device - for node in child_nodes: - try: - parent_device = devices_by_nid[node.parent_node.nid] - except KeyError: - _LOGGER.error( - "Node %s has a parent node %s, but no device " - "was created for the parent. Skipping.", - node.nid, - node.parent_nid, - ) - else: - device_type = _detect_device_type(node) - subnode_id = int(node.nid[-1], 16) - if device_type in ("opening", "moisture"): - # These sensors use an optional "negative" subnode 2 to snag - # all state changes - if subnode_id == 2: - parent_device.add_negative_node(node) - elif subnode_id == 4: - # Subnode 4 is the heartbeat node, which we will represent - # as a separate binary_sensor - device = ISYBinarySensorHeartbeat(node, parent_device) - parent_device.add_heartbeat_device(device) - devices.append(device) - else: - # We don't yet have any special logic for other sensor types, - # so add the nodes as individual devices - device = ISYBinarySensorDevice(node) + # Handle some special child node cases for Insteon Devices + for (node, device_class, device_type) in child_nodes: + subnode_id = int(node.address.split(" ")[-1], 16) + # Handle Insteon Thermostats + if device_type.startswith(TYPE_CATEGORY_CLIMATE): + if subnode_id == SUBNODE_CLIMATE_COOL: + # Subnode 2 is the "Cool Control" sensor + # It never reports its state until first use is + # detected after an ISY Restart, so we assume it's off. + # As soon as the ISY Event Stream connects if it has a + # valid state, it will be set. + device = ISYInsteonBinarySensorEntity(node, DEVICE_CLASS_COLD, False) devices.append(device) + elif subnode_id == SUBNODE_CLIMATE_HEAT: + # Subnode 3 is the "Heat Control" sensor + device = ISYInsteonBinarySensorEntity(node, DEVICE_CLASS_HEAT, False) + devices.append(device) + continue - for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYBinarySensorProgram(name, status)) + if device_class in DEVICE_PARENT_REQUIRED: + parent_device = devices_by_address.get(node.parent_node.address) + if not parent_device: + _LOGGER.error( + "Node %s has a parent node %s, but no device " + "was created for the parent. Skipping.", + node.address, + node.parent_node, + ) + continue - add_entities(devices) + if device_class in (DEVICE_CLASS_OPENING, DEVICE_CLASS_MOISTURE): + # These sensors use an optional "negative" subnode 2 to + # snag all state changes + if subnode_id == SUBNODE_NEGATIVE: + parent_device.add_negative_node(node) + elif subnode_id == SUBNODE_HEARTBEAT: + # Subnode 4 is the heartbeat node, which we will + # represent as a separate binary_sensor + device = ISYBinarySensorHeartbeat(node, parent_device) + parent_device.add_heartbeat_device(device) + devices.append(device) + continue + if ( + device_class == DEVICE_CLASS_MOTION + and device_type is not None + and any([device_type.startswith(t) for t in TYPE_INSTEON_MOTION]) + ): + # Special cases for Insteon Motion Sensors I & II: + # Some subnodes never report status until activated, so + # the initial state is forced "OFF"/"NORMAL" if the + # parent device has a valid state. This is corrected + # upon connection to the ISY event stream if subnode has a valid state. + initial_state = None if parent_device.state is None else False + if subnode_id == SUBNODE_DUSK_DAWN: + # Subnode 2 is the Dusk/Dawn sensor + device = ISYInsteonBinarySensorEntity(node, DEVICE_CLASS_LIGHT) + devices.append(device) + continue + if subnode_id == SUBNODE_LOW_BATTERY: + # Subnode 3 is the low battery node + device = ISYInsteonBinarySensorEntity( + node, DEVICE_CLASS_BATTERY, initial_state + ) + devices.append(device) + continue + if subnode_id in SUBNODE_TAMPER: + # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes + # reported as "10", which translate from Hex to 10 and 16 resp. + device = ISYInsteonBinarySensorEntity( + node, DEVICE_CLASS_PROBLEM, initial_state + ) + devices.append(device) + continue + if subnode_id in SUBNODE_MOTION_DISABLED: + # Motion Disabled Sub-node for MS II ("D" or "13") + device = ISYInsteonBinarySensorEntity(node) + devices.append(device) + continue + + # We don't yet have any special logic for other sensor + # types, so add the nodes as individual devices + device = ISYBinarySensorEntity(node, device_class) + devices.append(device) + + for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]: + devices.append(ISYBinarySensorProgramEntity(name, status)) + + await migrate_old_unique_ids(hass, BINARY_SENSOR, devices) + async_add_entities(devices) + async_setup_device_services(hass) -def _detect_device_type(node) -> str: +def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str): try: device_type = node.type except AttributeError: # The type attribute didn't exist in the ISY's API response - return None + return (None, None) - split_type = device_type.split(".") - for device_class, ids in ISY_DEVICE_TYPES.items(): - if f"{split_type[0]}.{split_type[1]}" in ids: - return device_class + # Z-Wave Devices: + if node.protocol == PROTO_ZWAVE: + device_type = f"Z{node.zwave_props.category}" + for device_class in [*BINARY_SENSOR_DEVICE_TYPES_ZWAVE]: + if ( + node.zwave_props.category + in BINARY_SENSOR_DEVICE_TYPES_ZWAVE[device_class] + ): + return device_class, device_type + return (None, device_type) - return None + # Other devices (incl Insteon.) + for device_class in [*BINARY_SENSOR_DEVICE_TYPES_ISY]: + if any( + [ + device_type.startswith(t) + for t in set(BINARY_SENSOR_DEVICE_TYPES_ISY[device_class]) + ] + ): + return device_class, device_type + return (None, device_type) -def _is_val_unknown(val): - """Determine if a number value represents UNKNOWN from PyISY.""" - return val == -1 * float("inf") +class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): + """Representation of a basic ISY994 binary sensor device.""" + + def __init__(self, node, force_device_class=None, unknown_state=None) -> None: + """Initialize the ISY994 binary sensor device.""" + super().__init__(node) + self._device_class = force_device_class + + @property + def is_on(self) -> bool: + """Get whether the ISY994 binary sensor device is on.""" + if self._node.status == ISY_VALUE_UNKNOWN: + return None + return bool(self._node.status) + + @property + def device_class(self) -> str: + """Return the class of this device. + + This was discovered by parsing the device type code during init + """ + return self._device_class -class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): - """Representation of an ISY994 binary sensor device. +class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): + """Representation of an ISY994 Insteon binary sensor device. Often times, a single device is represented by multiple nodes in the ISY, allowing for different nuances in how those devices report their on and - off events. This class turns those multiple nodes in to a single Home + off events. This class turns those multiple nodes into a single Home Assistant entity and handles both ways that ISY binary sensors can work. """ - def __init__(self, node) -> None: + def __init__(self, node, force_device_class=None, unknown_state=None) -> None: """Initialize the ISY994 binary sensor device.""" - super().__init__(node) + super().__init__(node, force_device_class) self._negative_node = None self._heartbeat_device = None - self._device_class_from_type = _detect_device_type(self._node) - if _is_val_unknown(self._node.status._val): - self._computed_state = None + if self._node.status == ISY_VALUE_UNKNOWN: + self._computed_state = unknown_state self._status_was_unknown = True else: - self._computed_state = bool(self._node.status._val) + self._computed_state = bool(self._node.status) self._status_was_unknown = False async def async_added_to_hass(self) -> None: """Subscribe to the node and subnode event emitters.""" await super().async_added_to_hass() - self._node.controlEvents.subscribe(self._positive_node_control_handler) + self._node.control_events.subscribe(self._positive_node_control_handler) if self._negative_node is not None: - self._negative_node.controlEvents.subscribe( + self._negative_node.control_events.subscribe( self._negative_node_control_handler ) @@ -150,20 +283,19 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): """ self._negative_node = child - # pylint: disable=protected-access - if not _is_val_unknown(self._negative_node.status._val): + if self._negative_node.status != ISY_VALUE_UNKNOWN: # If the negative node has a value, it means the negative node is # in use for this device. Next we need to check to see if the # negative and positive nodes disagree on the state (both ON or # both OFF). - if self._negative_node.status._val == self._node.status._val: + if self._negative_node.status == self._node.status: # The states disagree, therefore we cannot determine the state # of the sensor until we receive our first ON event. self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" - if event == "DON": + if event.control == CMD_ON: _LOGGER.debug( "Sensor %s turning Off via the Negative node sending a DON command", self.name, @@ -179,7 +311,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): will come to this node, with the negative node representing Off events """ - if event == "DON": + if event.control == CMD_ON: _LOGGER.debug( "Sensor %s turning On via the Primary node sending a DON command", self.name, @@ -187,7 +319,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self._computed_state = True self.schedule_update_ha_state() self._heartbeat() - if event == "DOF": + if event.control == CMD_OFF: _LOGGER.debug( "Sensor %s turning Off via the Primary node sending a DOF command", self.name, @@ -207,14 +339,14 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): an accompanying Control event, so we need to watch for it. """ if self._status_was_unknown and self._computed_state is None: - self._computed_state = bool(int(self._node.status)) + self._computed_state = bool(self._node.status) self._status_was_unknown = False self.schedule_update_ha_state() self._heartbeat() @property - def value(self) -> object: - """Get the current value of the device. + def is_on(self) -> bool: + """Get whether the ISY994 binary sensor device is on. Insteon leak sensors set their primary node to On when the state is DRY, not WET, so we invert the binary state if the user indicates @@ -224,57 +356,46 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # Do this first so we don't invert None on moisture sensors return None - if self.device_class == "moisture": + if self.device_class == DEVICE_CLASS_MOISTURE: return not self._computed_state return self._computed_state - @property - def is_on(self) -> bool: - """Get whether the ISY994 binary sensor device is on. - Note: This method will return false if the current state is UNKNOWN - """ - return bool(self.value) - - @property - def state(self): - """Return the state of the binary sensor.""" - if self._computed_state is None: - return None - return STATE_ON if self.is_on else STATE_OFF - - @property - def device_class(self) -> str: - """Return the class of this device. - - This was discovered by parsing the device type code during init - """ - return self._device_class_from_type - - -class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): +class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """Representation of the battery state of an ISY994 sensor.""" def __init__(self, node, parent_device) -> None: - """Initialize the ISY994 binary sensor device.""" + """Initialize the ISY994 binary sensor device. + + Computed state is set to UNKNOWN unless the ISY provided a valid + state. See notes above regarding ISY Sensor status on ISY restart. + If a valid state is provided (either on or off), the computed state in + HA is set to OFF (Normal). If the heartbeat is not received in 25 hours + then the computed state is set to ON (Low Battery). + """ super().__init__(node) - self._computed_state = None self._parent_device = parent_device self._heartbeat_timer = None + self._computed_state = None + if self.state is None: + self._computed_state = False async def async_added_to_hass(self) -> None: """Subscribe to the node and subnode event emitters.""" await super().async_added_to_hass() - self._node.controlEvents.subscribe(self._heartbeat_node_control_handler) + self._node.control_events.subscribe(self._heartbeat_node_control_handler) - # Start the timer on bootup, so we can change from UNKNOWN to ON + # Start the timer on bootup, so we can change from UNKNOWN to OFF self._restart_timer() def _heartbeat_node_control_handler(self, event: object) -> None: - """Update the heartbeat timestamp when an On event is sent.""" - if event == "DON": + """Update the heartbeat timestamp when any ON/OFF event is sent. + + The ISY uses both DON and DOF commands (alternating) for a heartbeat. + """ + if event.control in [CMD_ON, CMD_OFF]: self.heartbeat() def heartbeat(self): @@ -300,14 +421,16 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): @callback def timer_elapsed(now) -> None: - """Heartbeat missed; set state to indicate dead battery.""" + """Heartbeat missed; set state to ON to indicate dead battery.""" self._computed_state = True self._heartbeat_timer = None self.schedule_update_ha_state() point_in_time = dt_util.utcnow() + timedelta(hours=25) _LOGGER.debug( - "Timer starting. Now: %s Then: %s", dt_util.utcnow(), point_in_time + "Heartbeat timer starting. Now: %s Then: %s", + dt_util.utcnow(), + point_in_time, ) self._heartbeat_timer = async_track_point_in_utc_time( @@ -320,30 +443,20 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): We listen directly to the Control events for this device. """ - @property - def value(self) -> object: - """Get the current value of this sensor.""" - return self._computed_state - @property def is_on(self) -> bool: """Get whether the ISY994 binary sensor device is on. Note: This method will return false if the current state is UNKNOWN + which occurs after a restart until the first heartbeat or control + parent control event is received. """ - return bool(self.value) - - @property - def state(self): - """Return the state of the binary sensor.""" - if self._computed_state is None: - return None - return STATE_ON if self.is_on else STATE_OFF + return bool(self._computed_state) @property def device_class(self) -> str: """Get the class of this device.""" - return "battery" + return DEVICE_CLASS_BATTERY @property def device_state_attributes(self): @@ -353,19 +466,14 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): return attr -class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice): +class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): """Representation of an ISY994 binary sensor program. This does not need all of the subnode logic in the device version of binary sensors. """ - def __init__(self, name, node) -> None: - """Initialize the ISY994 binary sensor program.""" - super().__init__(node) - self._name = name - @property def is_on(self) -> bool: """Get whether the ISY994 binary sensor device is on.""" - return bool(self.value) + return bool(self._node.status) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py new file mode 100644 index 00000000000..8299265a381 --- /dev/null +++ b/homeassistant/components/isy994/climate.py @@ -0,0 +1,241 @@ +"""Support for Insteon Thermostats via ISY994 Platform.""" +from typing import Callable, List, Optional + +from pyisy.constants import ( + CMD_CLIMATE_FAN_SETTING, + CMD_CLIMATE_MODE, + PROP_HEAT_COOL_STATE, + PROP_HUMIDITY, + PROP_SETPOINT_COOL, + PROP_SETPOINT_HEAT, + PROP_UOM, + PROTO_INSTEON, +) + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE, + FAN_AUTO, + FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + _LOGGER, + DOMAIN as ISY994_DOMAIN, + HA_FAN_TO_ISY, + HA_HVAC_TO_ISY, + ISY994_NODES, + ISY_HVAC_MODES, + UOM_FAN_MODES, + UOM_HVAC_ACTIONS, + UOM_HVAC_MODE_GENERIC, + UOM_HVAC_MODE_INSTEON, + UOM_ISY_CELSIUS, + UOM_ISY_FAHRENHEIT, + UOM_ISYV4_NONE, + UOM_TO_STATES, +) +from .entity import ISYNodeEntity +from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +from .services import async_setup_device_services + +ISY_SUPPORTED_FEATURES = ( + SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE +) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: + """Set up the ISY994 thermostat platform.""" + entities = [] + + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + for node in hass_isy_data[ISY994_NODES][CLIMATE]: + entities.append(ISYThermostatEntity(node)) + + await migrate_old_unique_ids(hass, CLIMATE, entities) + async_add_entities(entities) + async_setup_device_services(hass) + + +class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): + """Representation of an ISY994 thermostat entity.""" + + def __init__(self, node) -> None: + """Initialize the ISY Thermostat entity.""" + super().__init__(node) + self._node = node + self._uom = self._node.uom + if isinstance(self._uom, list): + self._uom = self._node.uom[0] + self._hvac_action = None + self._hvac_mode = None + self._fan_mode = None + self._temp_unit = None + self._current_humidity = 0 + self._target_temp_low = 0 + self._target_temp_high = 0 + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ISY_SUPPORTED_FEATURES + + @property + def precision(self) -> str: + """Return the precision of the system.""" + return PRECISION_TENTHS + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + uom = self._node.aux_properties.get(PROP_UOM) + if not uom: + return self.hass.config.units.temperature_unit + if uom.value == UOM_ISY_CELSIUS: + return TEMP_CELSIUS + if uom.value == UOM_ISY_FAHRENHEIT: + return TEMP_FAHRENHEIT + + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity.""" + humidity = self._node.aux_properties.get(PROP_HUMIDITY) + if not humidity: + return None + return int(humidity.value) + + @property + def hvac_mode(self) -> Optional[str]: + """Return hvac operation ie. heat, cool mode.""" + hvac_mode = self._node.aux_properties.get(CMD_CLIMATE_MODE) + if not hvac_mode: + return None + + # Which state values used depends on the mode property's UOM: + uom = hvac_mode.uom + # Handle special case for ISYv4 Firmware: + if uom == UOM_ISYV4_NONE: + uom = ( + UOM_HVAC_MODE_INSTEON + if self._node.protocol == PROTO_INSTEON + else UOM_HVAC_MODE_GENERIC + ) + return UOM_TO_STATES[uom].get(hvac_mode.value) + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return ISY_HVAC_MODES + + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + hvac_action = self._node.aux_properties.get(PROP_HEAT_COOL_STATE) + if not hvac_action: + return None + return UOM_TO_STATES[UOM_HVAC_ACTIONS].get(hvac_action.value) + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return convert_isy_value_to_hass( + self._node.status, self._uom, self._node.prec, 1 + ) + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return 1.0 + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + if self.hvac_mode == HVAC_MODE_COOL: + return self.target_temperature_high + if self.hvac_mode == HVAC_MODE_HEAT: + return self.target_temperature_low + return None + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + target = self._node.aux_properties.get(PROP_SETPOINT_COOL) + if not target: + return None + return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1) + + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + target = self._node.aux_properties.get(PROP_SETPOINT_HEAT) + if not target: + return None + return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1) + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return [FAN_AUTO, FAN_ON] + + @property + def fan_mode(self) -> str: + """Return the current fan mode ie. auto, on.""" + fan_mode = self._node.aux_properties.get(CMD_CLIMATE_FAN_SETTING) + if not fan_mode: + return None + return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value) + + def set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp is not None: + if self.hvac_mode == HVAC_MODE_COOL: + target_temp_high = target_temp + if self.hvac_mode == HVAC_MODE_HEAT: + target_temp_low = target_temp + if target_temp_low is not None: + self._node.set_climate_setpoint_heat(int(target_temp_low)) + # Presumptive setting--event stream will correct if cmd fails: + self._target_temp_low = target_temp_low + if target_temp_high is not None: + self._node.set_climate_setpoint_cool(int(target_temp_high)) + # Presumptive setting--event stream will correct if cmd fails: + self._target_temp_high = target_temp_high + self.schedule_update_ha_state() + + def set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + _LOGGER.debug("Requested fan mode %s", fan_mode) + self._node.set_fan_mode(HA_FAN_TO_ISY.get(fan_mode)) + # Presumptive setting--event stream will correct if cmd fails: + self._fan_mode = fan_mode + self.schedule_update_ha_state() + + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + _LOGGER.debug("Requested operation mode %s", hvac_mode) + self._node.set_climate_mode(HA_HVAC_TO_ISY.get(hvac_mode)) + # Presumptive setting--event stream will correct if cmd fails: + self._hvac_mode = hvac_mode + self.schedule_update_ha_state() diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py new file mode 100644 index 00000000000..0ed1d7e6833 --- /dev/null +++ b/homeassistant/components/isy994/config_flow.py @@ -0,0 +1,222 @@ +"""Config flow for Universal Devices ISY994 integration.""" +import logging +from urllib.parse import urlparse + +from pyisy.configuration import Configuration +from pyisy.connection import Connection +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import ( + CONF_IGNORE_STRING, + CONF_RESTORE_LIGHT_STATE, + CONF_SENSOR_STRING, + CONF_TLS_VER, + CONF_VAR_SENSOR_STRING, + DEFAULT_IGNORE_STRING, + DEFAULT_RESTORE_LIGHT_STATE, + DEFAULT_SENSOR_STRING, + DEFAULT_TLS_VERSION, + DEFAULT_VAR_SENSOR_STRING, + ISY_URL_POSTFIX, + UDN_UUID_PREFIX, +) +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +def _data_schema(schema_input): + """Generate schema with defaults.""" + return vol.Schema( + { + vol.Required(CONF_HOST, default=schema_input.get(CONF_HOST, "")): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_TLS_VER, default=DEFAULT_TLS_VERSION): vol.In([1.1, 1.2]), + }, + extra=vol.ALLOW_EXTRA, + ) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + user = data[CONF_USERNAME] + password = data[CONF_PASSWORD] + host = urlparse(data[CONF_HOST]) + tls_version = data.get(CONF_TLS_VER) + + if host.scheme == "http": + https = False + port = host.port or 80 + elif host.scheme == "https": + https = True + port = host.port or 443 + else: + _LOGGER.error("isy994 host value in configuration is invalid") + raise InvalidHost + + # Connect to ISY controller. + isy_conf = await hass.async_add_executor_job( + _fetch_isy_configuration, + host.hostname, + port, + user, + password, + https, + tls_version, + host.path, + ) + + if not isy_conf or "name" not in isy_conf or not isy_conf["name"]: + raise CannotConnect + + # Return info that you want to store in the config entry. + return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]} + + +def _fetch_isy_configuration( + address, port, username, password, use_https, tls_ver, webroot +): + """Validate and fetch the configuration from the ISY.""" + try: + isy_conn = Connection( + address, + port, + username, + password, + use_https, + tls_ver, + log=_LOGGER, + webroot=webroot, + ) + except ValueError as err: + raise InvalidAuth(err.args[0]) + + return Configuration(log=_LOGGER, xml=isy_conn.get_config()) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Universal Devices ISY994.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the isy994 config flow.""" + self.discovered_conf = {} + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + info = None + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidHost: + errors["base"] = "invalid_host" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(info["uuid"], raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=_data_schema(self.discovered_conf), + errors=errors, + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered isy994.""" + friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] + url = discovery_info[ssdp.ATTR_SSDP_LOCATION] + mac = discovery_info[ssdp.ATTR_UPNP_UDN] + if mac.startswith(UDN_UUID_PREFIX): + mac = mac[len(UDN_UUID_PREFIX) :] + if url.endswith(ISY_URL_POSTFIX): + url = url[: -len(ISY_URL_POSTFIX)] + + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + + self.discovered_conf = { + CONF_NAME: friendly_name, + CONF_HOST: url, + } + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = self.discovered_conf + return await self.async_step_user() + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for isy994.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = self.config_entry.options + restore_light_state = options.get( + CONF_RESTORE_LIGHT_STATE, DEFAULT_RESTORE_LIGHT_STATE + ) + ignore_string = options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING) + sensor_string = options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING) + var_sensor_string = options.get( + CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING + ) + + options_schema = vol.Schema( + { + vol.Optional(CONF_IGNORE_STRING, default=ignore_string): str, + vol.Optional(CONF_SENSOR_STRING, default=sensor_string): str, + vol.Optional(CONF_VAR_SENSOR_STRING, default=var_sensor_string): str, + vol.Required( + CONF_RESTORE_LIGHT_STATE, default=restore_light_state + ): bool, + } + ) + + return self.async_show_form(step_id="init", data_schema=options_schema) + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate the host value is invalid.""" + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py new file mode 100644 index 00000000000..f7042a5860a --- /dev/null +++ b/homeassistant/components/isy994/const.py @@ -0,0 +1,647 @@ +"""Constants for the ISY994 Platform.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DOMAIN as BINARY_SENSOR, +) +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DOMAIN as CLIMATE, + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_ON, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_BOOST, +) +from homeassistant.components.cover import DOMAIN as COVER +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + MASS_KILOGRAMS, + MASS_POUNDS, + POWER_WATT, + PRESSURE_INHG, + SERVICE_LOCK, + SERVICE_UNLOCK, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + STATE_CLOSED, + STATE_CLOSING, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_OPENING, + STATE_PROBLEM, + STATE_UNKNOWN, + STATE_UNLOCKED, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, + TIME_DAYS, + TIME_HOURS, + TIME_MILLISECONDS, + TIME_MINUTES, + TIME_MONTHS, + TIME_SECONDS, + TIME_YEARS, + UNIT_PERCENTAGE, + UV_INDEX, + VOLT, + VOLUME_GALLONS, + VOLUME_LITERS, +) + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "isy994" + +MANUFACTURER = "Universal Devices, Inc" + +CONF_IGNORE_STRING = "ignore_string" +CONF_SENSOR_STRING = "sensor_string" +CONF_VAR_SENSOR_STRING = "variable_sensor_string" +CONF_TLS_VER = "tls" +CONF_RESTORE_LIGHT_STATE = "restore_light_state" + +DEFAULT_IGNORE_STRING = "{IGNORE ME}" +DEFAULT_SENSOR_STRING = "sensor" +DEFAULT_RESTORE_LIGHT_STATE = False +DEFAULT_TLS_VERSION = 1.1 +DEFAULT_PROGRAM_STRING = "HA." +DEFAULT_VAR_SENSOR_STRING = "HA." + +KEY_ACTIONS = "actions" +KEY_STATUS = "status" + +SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE] +SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] + +SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] + +# ISY Scenes are more like Switches than Home Assistant Scenes +# (they can turn off, and report their state) +ISY_GROUP_PLATFORM = SWITCH + +ISY994_ISY = "isy" +ISY994_NODES = "isy994_nodes" +ISY994_PROGRAMS = "isy994_programs" +ISY994_VARIABLES = "isy994_variables" + +FILTER_UOM = "uom" +FILTER_STATES = "states" +FILTER_NODE_DEF_ID = "node_def_id" +FILTER_INSTEON_TYPE = "insteon_type" +FILTER_ZWAVE_CAT = "zwave_cat" + +# Special Subnodes for some Insteon Devices +SUBNODE_CLIMATE_COOL = 2 +SUBNODE_CLIMATE_HEAT = 3 +SUBNODE_DUSK_DAWN = 2 +SUBNODE_EZIO2X4_SENSORS = [9, 10, 11, 12] +SUBNODE_FANLINC_LIGHT = 1 +SUBNODE_HEARTBEAT = 4 +SUBNODE_IOLINC_RELAY = 2 +SUBNODE_LOW_BATTERY = 3 +SUBNODE_MOTION_DISABLED = (13, 19) # Int->13 or Hex->0xD depending on firmware +SUBNODE_NEGATIVE = 2 +SUBNODE_TAMPER = (10, 16) # Int->10 or Hex->0xA depending on firmware + +# Generic Insteon Type Categories for Filters +TYPE_CATEGORY_CONTROLLERS = "0." +TYPE_CATEGORY_DIMMABLE = "1." +TYPE_CATEGORY_SWITCHED = "2." +TYPE_CATEGORY_IRRIGATION = "4." +TYPE_CATEGORY_CLIMATE = "5." +TYPE_CATEGORY_POOL_CTL = "6." +TYPE_CATEGORY_SENSOR_ACTUATORS = "7." +TYPE_CATEGORY_ENERGY_MGMT = "9." +TYPE_CATEGORY_COVER = "14." +TYPE_CATEGORY_LOCK = "15." +TYPE_CATEGORY_SAFETY = "16." +TYPE_CATEGORY_X10 = "113." + +TYPE_EZIO2X4 = "7.3.255." +TYPE_INSTEON_MOTION = ("16.1.", "16.22.") + +UNDO_UPDATE_LISTENER = "undo_update_listener" + +# Used for discovery +UDN_UUID_PREFIX = "uuid:" +ISY_URL_POSTFIX = "/desc" + +# Do not use the Home Assistant consts for the states here - we're matching exact API +# responses, not using them for Home Assistant states +# Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml +# Z-Wave Categories: https://www.universal-devices.com/developers/wsdk/5.0.4/4_fam.xml +NODE_FILTERS = { + BINARY_SENSOR: { + FILTER_UOM: [], + FILTER_STATES: [], + FILTER_NODE_DEF_ID: [ + "BinaryAlarm", + "BinaryAlarm_ADV", + "BinaryControl", + "BinaryControl_ADV", + "EZIO2x4_Input", + "EZRAIN_Input", + "OnOffControl", + "OnOffControl_ADV", + ], + FILTER_INSTEON_TYPE: [ + "7.0.", + "7.13.", + TYPE_CATEGORY_SAFETY, + ], # Does a startswith() match; include the dot + FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))), + }, + SENSOR: { + # This is just a more-readable way of including MOST uoms between 1-100 + # (Remember that range() is non-inclusive of the stop value) + FILTER_UOM: ( + ["1"] + + list(map(str, range(3, 11))) + + list(map(str, range(12, 51))) + + list(map(str, range(52, 66))) + + list(map(str, range(69, 78))) + + ["79"] + + list(map(str, range(82, 97))) + ), + FILTER_STATES: [], + FILTER_NODE_DEF_ID: [ + "IMETER_SOLO", + "EZIO2x4_Input_ADV", + "KeypadButton", + "KeypadButton_ADV", + "RemoteLinc2", + "RemoteLinc2_ADV", + ], + FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."], + FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 185)))), + }, + LOCK: { + FILTER_UOM: ["11"], + FILTER_STATES: ["locked", "unlocked"], + FILTER_NODE_DEF_ID: ["DoorLock"], + FILTER_INSTEON_TYPE: [TYPE_CATEGORY_LOCK, "4.64."], + FILTER_ZWAVE_CAT: ["111"], + }, + FAN: { + FILTER_UOM: [], + FILTER_STATES: ["off", "low", "med", "high"], + FILTER_NODE_DEF_ID: ["FanLincMotor"], + FILTER_INSTEON_TYPE: ["1.46."], + FILTER_ZWAVE_CAT: [], + }, + COVER: { + FILTER_UOM: ["97"], + FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"], + FILTER_NODE_DEF_ID: [], + FILTER_INSTEON_TYPE: [], + FILTER_ZWAVE_CAT: [], + }, + LIGHT: { + FILTER_UOM: ["51"], + FILTER_STATES: ["on", "off", "%"], + FILTER_NODE_DEF_ID: [ + "BallastRelayLampSwitch", + "BallastRelayLampSwitch_ADV", + "DimmerLampOnly", + "DimmerLampSwitch", + "DimmerLampSwitch_ADV", + "DimmerSwitchOnly", + "DimmerSwitchOnly_ADV", + "KeypadDimmer", + "KeypadDimmer_ADV", + ], + FILTER_INSTEON_TYPE: [TYPE_CATEGORY_DIMMABLE], + FILTER_ZWAVE_CAT: ["109", "119"], + }, + SWITCH: { + FILTER_UOM: ["2", "78"], + FILTER_STATES: ["on", "off"], + FILTER_NODE_DEF_ID: [ + "AlertModuleArmed", + "AlertModuleSiren", + "AlertModuleSiren_ADV", + "EZIO2x4_Output", + "EZRAIN_Output", + "KeypadRelay", + "KeypadRelay_ADV", + "RelayLampOnly", + "RelayLampOnly_ADV", + "RelayLampSwitch", + "RelayLampSwitch_ADV", + "RelaySwitchOnlyPlusQuery", + "RelaySwitchOnlyPlusQuery_ADV", + "Siren", + "Siren_ADV", + "X10", + ], + FILTER_INSTEON_TYPE: [ + TYPE_CATEGORY_SWITCHED, + "7.3.255.", + "9.10.", + "9.11.", + TYPE_CATEGORY_X10, + ], + FILTER_ZWAVE_CAT: ["121", "122", "123", "137", "141", "147"], + }, + CLIMATE: { + FILTER_UOM: ["2"], + FILTER_STATES: ["heating", "cooling", "idle", "fan_only", "off"], + FILTER_NODE_DEF_ID: ["TempLinc", "Thermostat"], + FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE], + FILTER_ZWAVE_CAT: ["140"], + }, +} + +UOM_ISYV4_DEGREES = "degrees" +UOM_ISYV4_NONE = "n/a" + +UOM_ISY_CELSIUS = 1 +UOM_ISY_FAHRENHEIT = 2 + +UOM_DOUBLE_TEMP = "101" +UOM_HVAC_ACTIONS = "66" +UOM_HVAC_MODE_GENERIC = "67" +UOM_HVAC_MODE_INSTEON = "98" +UOM_FAN_MODES = "99" +UOM_INDEX = "25" +UOM_ON_OFF = "2" + +UOM_FRIENDLY_NAME = { + "1": "A", + "3": f"btu/{TIME_HOURS}", + "4": TEMP_CELSIUS, + "5": LENGTH_CENTIMETERS, + "6": "ft³", + "7": f"ft³/{TIME_MINUTES}", + "8": "m³", + "9": TIME_DAYS, + "10": TIME_DAYS, + "12": "dB", + "13": "dB A", + "14": DEGREE, + "16": "macroseismic", + "17": TEMP_FAHRENHEIT, + "18": LENGTH_FEET, + "19": TIME_HOURS, + "20": TIME_HOURS, + "21": "%AH", + "22": "%RH", + "23": PRESSURE_INHG, + "24": f"{LENGTH_INCHES}/{TIME_HOURS}", + UOM_INDEX: "index", # Index type. Use "node.formatted" for value + "26": TEMP_KELVIN, + "27": "keyword", + "28": MASS_KILOGRAMS, + "29": "kV", + "30": "kW", + "31": "kPa", + "32": SPEED_KILOMETERS_PER_HOUR, + "33": ENERGY_KILO_WATT_HOUR, + "34": "liedu", + "35": VOLUME_LITERS, + "36": "lx", + "37": "mercalli", + "38": LENGTH_METERS, + "39": f"{LENGTH_METERS}³/{TIME_HOURS}", + "40": SPEED_METERS_PER_SECOND, + "41": "mA", + "42": TIME_MILLISECONDS, + "43": "mV", + "44": TIME_MINUTES, + "45": TIME_MINUTES, + "46": f"mm/{TIME_HOURS}", + "47": TIME_MONTHS, + "48": SPEED_MILES_PER_HOUR, + "49": SPEED_METERS_PER_SECOND, + "50": "Ω", + "51": UNIT_PERCENTAGE, + "52": MASS_POUNDS, + "53": "pf", + "54": CONCENTRATION_PARTS_PER_MILLION, + "55": "pulse count", + "57": TIME_SECONDS, + "58": TIME_SECONDS, + "59": "S/m", + "60": "m_b", + "61": "M_L", + "62": "M_w", + "63": "M_S", + "64": "shindo", + "65": "SML", + "69": VOLUME_GALLONS, + "71": UV_INDEX, + "72": VOLT, + "73": POWER_WATT, + "74": f"{POWER_WATT}/{LENGTH_METERS}²", + "75": "weekday", + "76": DEGREE, + "77": TIME_YEARS, + "82": "mm", + "83": LENGTH_KILOMETERS, + "85": "Ω", + "86": "kΩ", + "87": f"{LENGTH_METERS}³/{LENGTH_METERS}³", + "88": "Water activity", + "89": "RPM", + "90": FREQUENCY_HERTZ, + "91": DEGREE, + "92": f"{DEGREE} South", + "100": "", # Range 0-255, no unit. + UOM_DOUBLE_TEMP: UOM_DOUBLE_TEMP, + "102": "kWs", + "103": "$", + "104": "¢", + "105": LENGTH_INCHES, + "106": f"mm/{TIME_DAYS}", + "107": "", # raw 1-byte unsigned value + "108": "", # raw 2-byte unsigned value + "109": "", # raw 3-byte unsigned value + "110": "", # raw 4-byte unsigned value + "111": "", # raw 1-byte signed value + "112": "", # raw 2-byte signed value + "113": "", # raw 3-byte signed value + "114": "", # raw 4-byte signed value + "116": LENGTH_MILES, + "117": "mbar", + "118": "hPa", + "119": f"{POWER_WATT}{TIME_HOURS}", + "120": f"{LENGTH_INCHES}/{TIME_DAYS}", +} + +UOM_TO_STATES = { + "11": { # Deadbolt Status + 0: STATE_UNLOCKED, + 100: STATE_LOCKED, + 101: STATE_UNKNOWN, + 102: STATE_PROBLEM, + }, + "15": { # Door Lock Alarm + 1: "master code changed", + 2: "tamper code entry limit", + 3: "escutcheon removed", + 4: "key/manually locked", + 5: "locked by touch", + 6: "key/manually unlocked", + 7: "remote locking jammed bolt", + 8: "remotely locked", + 9: "remotely unlocked", + 10: "deadbolt jammed", + 11: "battery too low to operate", + 12: "critical low battery", + 13: "low battery", + 14: "automatically locked", + 15: "automatic locking jammed bolt", + 16: "remotely power cycled", + 17: "lock handling complete", + 19: "user deleted", + 20: "user added", + 21: "duplicate pin", + 22: "jammed bolt by locking with keypad", + 23: "locked by keypad", + 24: "unlocked by keypad", + 25: "keypad attempt outside schedule", + 26: "hardware failure", + 27: "factory reset", + }, + UOM_HVAC_ACTIONS: { # Thermostat Heat/Cool State + 0: CURRENT_HVAC_IDLE, + 1: CURRENT_HVAC_HEAT, + 2: CURRENT_HVAC_COOL, + 3: CURRENT_HVAC_FAN, + 4: CURRENT_HVAC_HEAT, # Pending Heat + 5: CURRENT_HVAC_COOL, # Pending Cool + # >6 defined in ISY but not implemented, leaving for future expanision. + 6: CURRENT_HVAC_IDLE, + 7: CURRENT_HVAC_HEAT, + 8: CURRENT_HVAC_HEAT, + 9: CURRENT_HVAC_COOL, + 10: CURRENT_HVAC_HEAT, + 11: CURRENT_HVAC_HEAT, + }, + UOM_HVAC_MODE_GENERIC: { # Thermostat Mode + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_AUTO, + 4: PRESET_BOOST, + 5: "resume", + 6: HVAC_MODE_FAN_ONLY, + 7: "furnace", + 8: HVAC_MODE_DRY, + 9: "moist air", + 10: "auto changeover", + 11: "energy save heat", + 12: "energy save cool", + 13: PRESET_AWAY, + 14: HVAC_MODE_AUTO, + 15: HVAC_MODE_AUTO, + 16: HVAC_MODE_AUTO, + }, + "68": { # Thermostat Fan Mode + 0: FAN_AUTO, + 1: FAN_ON, + 2: FAN_HIGH, # Auto High + 3: FAN_HIGH, + 4: FAN_MEDIUM, # Auto Medium + 5: FAN_MEDIUM, + 6: "circulation", + 7: "humidity circulation", + }, + "78": {0: STATE_OFF, 100: STATE_ON}, # 0-Off 100-On + "79": {0: STATE_OPEN, 100: STATE_CLOSED}, # 0-Open 100-Close + "80": { # Thermostat Fan Run State + 0: STATE_OFF, + 1: STATE_ON, + 2: "on high", + 3: "on medium", + 4: "circulation", + 5: "humidity circulation", + 6: "right/left circulation", + 7: "up/down circulation", + 8: "quiet circulation", + }, + "84": {0: SERVICE_LOCK, 1: SERVICE_UNLOCK}, # Secure Mode + "93": { # Power Management Alarm + 1: "power applied", + 2: "ac mains disconnected", + 3: "ac mains reconnected", + 4: "surge detection", + 5: "volt drop or drift", + 6: "over current detected", + 7: "over voltage detected", + 8: "over load detected", + 9: "load error", + 10: "replace battery soon", + 11: "replace battery now", + 12: "battery is charging", + 13: "battery is fully charged", + 14: "charge battery soon", + 15: "charge battery now", + }, + "94": { # Appliance Alarm + 1: "program started", + 2: "program in progress", + 3: "program completed", + 4: "replace main filter", + 5: "failure to set target temperature", + 6: "supplying water", + 7: "water supply failure", + 8: "boiling", + 9: "boiling failure", + 10: "washing", + 11: "washing failure", + 12: "rinsing", + 13: "rinsing failure", + 14: "draining", + 15: "draining failure", + 16: "spinning", + 17: "spinning failure", + 18: "drying", + 19: "drying failure", + 20: "fan failure", + 21: "compressor failure", + }, + "95": { # Home Health Alarm + 1: "leaving bed", + 2: "sitting on bed", + 3: "lying on bed", + 4: "posture changed", + 5: "sitting on edge of bed", + }, + "96": { # VOC Level + 1: "clean", + 2: "slightly polluted", + 3: "moderately polluted", + 4: "highly polluted", + }, + "97": { # Barrier Status + **{ + 0: STATE_CLOSED, + 100: STATE_OPEN, + 101: STATE_UNKNOWN, + 102: "stopped", + 103: STATE_CLOSING, + 104: STATE_OPENING, + }, + **{ + b: f"{b} %" for a, b in enumerate(list(range(1, 100))) + }, # 1-99 are percentage open + }, + UOM_HVAC_MODE_INSTEON: { # Insteon Thermostat Mode + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_HEAT_COOL, + 4: HVAC_MODE_FAN_ONLY, + 5: HVAC_MODE_AUTO, # Program Auto + 6: HVAC_MODE_AUTO, # Program Heat-Set @ Local Device Only + 7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only + }, + UOM_FAN_MODES: {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode + "115": { # Most recent On style action taken for lamp control + 0: "on", + 1: "off", + 2: "fade up", + 3: "fade down", + 4: "fade stop", + 5: "fast on", + 6: "fast off", + 7: "triple press on", + 8: "triple press off", + 9: "4x press on", + 10: "4x press off", + 11: "5x press on", + 12: "5x press off", + }, +} + +ISY_HVAC_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, +] + +HA_HVAC_TO_ISY = { + HVAC_MODE_OFF: "off", + HVAC_MODE_HEAT: "heat", + HVAC_MODE_COOL: "cool", + HVAC_MODE_HEAT_COOL: "auto", + HVAC_MODE_FAN_ONLY: "fan_only", + HVAC_MODE_AUTO: "program_auto", +} + +HA_FAN_TO_ISY = {FAN_ON: "on", FAN_AUTO: "auto"} + +BINARY_SENSOR_DEVICE_TYPES_ISY = { + DEVICE_CLASS_MOISTURE: ["16.8.", "16.13.", "16.14."], + DEVICE_CLASS_OPENING: [ + "16.9.", + "16.6.", + "16.7.", + "16.2.", + "16.17.", + "16.20.", + "16.21.", + ], + DEVICE_CLASS_MOTION: ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."], +} + +BINARY_SENSOR_DEVICE_TYPES_ZWAVE = { + DEVICE_CLASS_SAFETY: ["137", "172", "176", "177", "178"], + DEVICE_CLASS_SMOKE: ["138", "156"], + DEVICE_CLASS_PROBLEM: ["148", "149", "157", "158", "164", "174", "175"], + DEVICE_CLASS_GAS: ["150", "151"], + DEVICE_CLASS_SOUND: ["153"], + DEVICE_CLASS_COLD: ["152", "168"], + DEVICE_CLASS_HEAT: ["154", "166", "167"], + DEVICE_CLASS_MOISTURE: ["159", "169"], + DEVICE_CLASS_DOOR: ["160"], + DEVICE_CLASS_BATTERY: ["162"], + DEVICE_CLASS_MOTION: ["155"], + DEVICE_CLASS_VIBRATION: ["173"], +} diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index f5e052f6926..e0e47592a37 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,97 +1,79 @@ """Support for ISY994 covers.""" -import logging from typing import Callable -from homeassistant.components.cover import DOMAIN, CoverDevice -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - STATE_UNKNOWN, -) -from homeassistant.helpers.typing import ConfigType +from pyisy.constants import ISY_VALUE_UNKNOWN -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice +from homeassistant.components.cover import DOMAIN as COVER, CoverEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -_LOGGER = logging.getLogger(__name__) - -VALUE_TO_STATE = { - 0: STATE_CLOSED, - 101: STATE_UNKNOWN, - 102: "stopped", - 103: STATE_CLOSING, - 104: STATE_OPENING, -} +from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .entity import ISYNodeEntity, ISYProgramEntity +from .helpers import migrate_old_unique_ids +from .services import async_setup_device_services -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 cover platform.""" + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYCoverDevice(node)) + for node in hass_isy_data[ISY994_NODES][COVER]: + devices.append(ISYCoverEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYCoverProgram(name, status, actions)) + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][COVER]: + devices.append(ISYCoverProgramEntity(name, status, actions)) - add_entities(devices) + await migrate_old_unique_ids(hass, COVER, devices) + async_add_entities(devices) + async_setup_device_services(hass) -class ISYCoverDevice(ISYDevice, CoverDevice): +class ISYCoverEntity(ISYNodeEntity, CoverEntity): """Representation of an ISY994 cover device.""" @property def current_cover_position(self) -> int: """Return the current cover position.""" - if self.is_unknown() or self.value is None: + if self._node.status == ISY_VALUE_UNKNOWN: return None - return sorted((0, self.value, 100))[1] + return sorted((0, self._node.status, 100))[1] @property def is_closed(self) -> bool: """Get whether the ISY994 cover device is closed.""" - return self.state == STATE_CLOSED - - @property - def state(self) -> str: - """Get the state of the ISY994 cover device.""" - if self.is_unknown(): + if self._node.status == ISY_VALUE_UNKNOWN: return None - return VALUE_TO_STATE.get(self.value, STATE_OPEN) + return self._node.status == 0 def open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover device.""" - if not self._node.on(val=100): + if not self._node.turn_on(val=100): _LOGGER.error("Unable to open the cover") def close_cover(self, **kwargs) -> None: """Send the close cover command to the ISY994 cover device.""" - if not self._node.off(): + if not self._node.turn_off(): _LOGGER.error("Unable to close the cover") -class ISYCoverProgram(ISYCoverDevice): +class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): """Representation of an ISY994 cover program.""" - def __init__(self, name: str, node: object, actions: object) -> None: - """Initialize the ISY994 cover program.""" - super().__init__(node) - self._name = name - self._actions = actions - @property - def state(self) -> str: - """Get the state of the ISY994 cover program.""" - return STATE_CLOSED if bool(self.value) else STATE_OPEN + def is_closed(self) -> bool: + """Get whether the ISY994 cover program is closed.""" + return bool(self._node.status) def open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover program.""" - if not self._actions.runThen(): + if not self._actions.run_then(): _LOGGER.error("Unable to open the cover") def close_cover(self, **kwargs) -> None: """Send the close cover command to the ISY994 cover program.""" - if not self._actions.runElse(): + if not self._actions.run_else(): _LOGGER.error("Unable to close the cover") diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py new file mode 100644 index 00000000000..a8805dc12cd --- /dev/null +++ b/homeassistant/components/isy994/entity.py @@ -0,0 +1,209 @@ +"""Representation of ISYEntity Types.""" + +from pyisy.constants import ( + COMMAND_FRIENDLY_NAME, + EMPTY_TIME, + EVENT_PROPS_IGNORED, + PROTO_GROUP, + PROTO_ZWAVE, +) +from pyisy.helpers import NodeProperty + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import Dict + +from .const import _LOGGER, DOMAIN + + +class ISYEntity(Entity): + """Representation of an ISY994 device.""" + + _name: str = None + + def __init__(self, node) -> None: + """Initialize the insteon device.""" + self._node = node + self._attrs = {} + self._change_handler = None + self._control_handler = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status_events.subscribe(self.on_update) + + if hasattr(self._node, "control_events"): + self._control_handler = self._node.control_events.subscribe(self.on_control) + + def on_update(self, event: object) -> None: + """Handle the update event from the ISY994 Node.""" + self.schedule_update_ha_state() + + def on_control(self, event: NodeProperty) -> None: + """Handle a control event from the ISY994 Node.""" + event_data = { + "entity_id": self.entity_id, + "control": event.control, + "value": event.value, + "formatted": event.formatted, + "uom": event.uom, + "precision": event.prec, + } + + if event.control not in EVENT_PROPS_IGNORED: + # New state attributes may be available, update the state. + self.schedule_update_ha_state() + + self.hass.bus.fire("isy994_control", event_data) + + @property + def device_info(self): + """Return the device_info of the device.""" + if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: + # not a device + return None + uuid = self._node.isy.configuration["uuid"] + node = self._node + basename = self.name + + if hasattr(self._node, "parent_node") and self._node.parent_node is not None: + # This is not the parent node, get the parent node. + node = self._node.parent_node + basename = node.name + + device_info = { + "name": basename, + "identifiers": {}, + "model": "Unknown", + "manufacturer": "Unknown", + "via_device": (DOMAIN, uuid), + } + + if hasattr(node, "address"): + device_info["name"] += f" ({node.address})" + if hasattr(node, "primary_node"): + device_info["identifiers"] = {(DOMAIN, f"{uuid}_{node.address}")} + # ISYv5 Device Types + if hasattr(node, "node_def_id") and node.node_def_id is not None: + device_info["model"] = node.node_def_id + # Numerical Device Type + if hasattr(node, "type") and node.type is not None: + device_info["model"] += f" {node.type}" + if hasattr(node, "protocol"): + device_info["manufacturer"] = node.protocol + if node.protocol == PROTO_ZWAVE: + # Get extra information for Z-Wave Devices + device_info["manufacturer"] += f" MfrID:{node.zwave_props.mfr_id}" + device_info["model"] += ( + f" Type:{node.zwave_props.devtype_gen} " + f"ProductTypeID:{node.zwave_props.prod_type_id} " + f"ProductID:{node.zwave_props.product_id}" + ) + # Note: sw_version is not exposed by the ISY for the individual devices. + + return device_info + + @property + def unique_id(self) -> str: + """Get the unique identifier of the device.""" + if hasattr(self._node, "address"): + return f"{self._node.isy.configuration['uuid']}_{self._node.address}" + return None + + @property + def old_unique_id(self) -> str: + """Get the old unique identifier of the device.""" + if hasattr(self._node, "address"): + return self._node.address + return None + + @property + def name(self) -> str: + """Get the name of the device.""" + return self._name or str(self._node.name) + + @property + def should_poll(self) -> bool: + """No polling required since we're using the subscription.""" + return False + + +class ISYNodeEntity(ISYEntity): + """Representation of a ISY Nodebase (Node/Group) entity.""" + + @property + def device_state_attributes(self) -> Dict: + """Get the state attributes for the device. + + The 'aux_properties' in the pyisy Node class are combined with the + other attributes which have been picked up from the event stream and + the combined result are returned as the device state attributes. + """ + attr = {} + if hasattr(self._node, "aux_properties"): + # Cast as list due to RuntimeError if a new property is added while running. + for name, value in list(self._node.aux_properties.items()): + attr_name = COMMAND_FRIENDLY_NAME.get(name, name) + attr[attr_name] = str(value.formatted).lower() + + # If a Group/Scene, set a property if the entire scene is on/off + if hasattr(self._node, "group_all_on"): + attr["group_all_on"] = STATE_ON if self._node.group_all_on else STATE_OFF + + self._attrs.update(attr) + return self._attrs + + def send_node_command(self, command): + """Respond to an entity service command call.""" + if not hasattr(self._node, command): + _LOGGER.error( + "Invalid Service Call %s for device %s.", command, self.entity_id + ) + return + getattr(self._node, command)() + + def send_raw_node_command( + self, command, value=None, unit_of_measurement=None, parameters=None + ): + """Respond to an entity service raw command call.""" + if not hasattr(self._node, "send_cmd"): + _LOGGER.error( + "Invalid Service Call %s for device %s.", command, self.entity_id + ) + return + self._node.send_cmd(command, value, unit_of_measurement, parameters) + + +class ISYProgramEntity(ISYEntity): + """Representation of an ISY994 program base.""" + + def __init__(self, name: str, status, actions=None) -> None: + """Initialize the ISY994 program-based entity.""" + super().__init__(status) + self._name = name + self._actions = actions + + @property + def device_state_attributes(self) -> Dict: + """Get the state attributes for the device.""" + attr = {} + if self._actions: + attr["actions_enabled"] = self._actions.enabled + if self._actions.last_finished != EMPTY_TIME: + attr["actions_last_finished"] = self._actions.last_finished + if self._actions.last_run != EMPTY_TIME: + attr["actions_last_run"] = self._actions.last_run + if self._actions.last_update != EMPTY_TIME: + attr["actions_last_update"] = self._actions.last_update + attr["ran_else"] = self._actions.ran_else + attr["ran_then"] = self._actions.ran_then + attr["run_at_startup"] = self._actions.run_at_startup + attr["running"] = self._actions.running + attr["status_enabled"] = self._node.enabled + if self._node.last_finished != EMPTY_TIME: + attr["status_last_finished"] = self._node.last_finished + if self._node.last_run != EMPTY_TIME: + attr["status_last_run"] = self._node.last_run + if self._node.last_update != EMPTY_TIME: + attr["status_last_update"] = self._node.last_update + return attr diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index b42e8cda755..96aa2144b1c 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,9 +1,10 @@ """Support for ISY994 fans.""" -import logging from typing import Callable +from pyisy.constants import ISY_VALUE_UNKNOWN + from homeassistant.components.fan import ( - DOMAIN, + DOMAIN as FAN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, @@ -11,11 +12,13 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .entity import ISYNodeEntity, ISYProgramEntity +from .helpers import migrate_old_unique_ids +from .services import async_setup_device_services VALUE_TO_STATE = { 0: SPEED_OFF, @@ -31,37 +34,44 @@ for key in VALUE_TO_STATE: STATE_TO_VALUE[VALUE_TO_STATE[key]] = key -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 fan platform.""" + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYFanDevice(node)) + for node in hass_isy_data[ISY994_NODES][FAN]: + devices.append(ISYFanEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYFanProgram(name, status, actions)) + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][FAN]: + devices.append(ISYFanProgramEntity(name, status, actions)) - add_entities(devices) + await migrate_old_unique_ids(hass, FAN, devices) + async_add_entities(devices) + async_setup_device_services(hass) -class ISYFanDevice(ISYDevice, FanEntity): +class ISYFanEntity(ISYNodeEntity, FanEntity): """Representation of an ISY994 fan device.""" @property def speed(self) -> str: """Return the current speed.""" - return VALUE_TO_STATE.get(self.value) + return VALUE_TO_STATE.get(self._node.status) @property def is_on(self) -> bool: """Get if the fan is on.""" - return self.value != 0 + if self._node.status == ISY_VALUE_UNKNOWN: + return None + return self._node.status != 0 def set_speed(self, speed: str) -> None: """Send the set speed command to the ISY994 fan device.""" - self._node.on(val=STATE_TO_VALUE.get(speed, 255)) + self._node.turn_on(val=STATE_TO_VALUE.get(speed, 255)) def turn_on(self, speed: str = None, **kwargs) -> None: """Send the turn on command to the ISY994 fan device.""" @@ -69,7 +79,7 @@ class ISYFanDevice(ISYDevice, FanEntity): def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 fan device.""" - self._node.off() + self._node.turn_off() @property def speed_list(self) -> list: @@ -82,26 +92,25 @@ class ISYFanDevice(ISYDevice, FanEntity): return SUPPORT_SET_SPEED -class ISYFanProgram(ISYFanDevice): +class ISYFanProgramEntity(ISYProgramEntity, FanEntity): """Representation of an ISY994 fan program.""" - def __init__(self, name: str, node, actions) -> None: - """Initialize the ISY994 fan program.""" - super().__init__(node) - self._name = name - self._actions = actions + @property + def speed(self) -> str: + """Return the current speed.""" + return VALUE_TO_STATE.get(self._node.status) + + @property + def is_on(self) -> bool: + """Get if the fan is on.""" + return self._node.status != 0 def turn_off(self, **kwargs) -> None: """Send the turn on command to ISY994 fan program.""" - if not self._actions.runThen(): + if not self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") def turn_on(self, speed: str = None, **kwargs) -> None: """Send the turn off command to ISY994 fan program.""" - if not self._actions.runElse(): + if not self._actions.run_else(): _LOGGER.error("Unable to turn on the fan") - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return 0 diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py new file mode 100644 index 00000000000..c8e39ec605d --- /dev/null +++ b/homeassistant/components/isy994/helpers.py @@ -0,0 +1,425 @@ +"""Sorting helpers for ISY994 device classifications.""" +from typing import Any, List, Optional, Union + +from pyisy.constants import ( + ISY_VALUE_UNKNOWN, + PROTO_GROUP, + PROTO_INSTEON, + PROTO_PROGRAM, + PROTO_ZWAVE, + TAG_FOLDER, +) +from pyisy.nodes import Group, Node, Nodes +from pyisy.programs import Programs +from pyisy.variables import Variables + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate.const import DOMAIN as CLIMATE +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + _LOGGER, + DEFAULT_PROGRAM_STRING, + DOMAIN, + FILTER_INSTEON_TYPE, + FILTER_NODE_DEF_ID, + FILTER_STATES, + FILTER_UOM, + FILTER_ZWAVE_CAT, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_VARIABLES, + ISY_GROUP_PLATFORM, + KEY_ACTIONS, + KEY_STATUS, + NODE_FILTERS, + SUBNODE_CLIMATE_COOL, + SUBNODE_CLIMATE_HEAT, + SUBNODE_EZIO2X4_SENSORS, + SUBNODE_FANLINC_LIGHT, + SUBNODE_IOLINC_RELAY, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, + TYPE_CATEGORY_SENSOR_ACTUATORS, + TYPE_EZIO2X4, + UOM_DOUBLE_TEMP, + UOM_ISYV4_DEGREES, +) + +BINARY_SENSOR_UOMS = ["2", "78"] +BINARY_SENSOR_ISY_STATES = ["on", "off"] + + +def _check_for_node_def( + hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None +) -> bool: + """Check if the node matches the node_def_id for any platforms. + + This is only present on the 5.0 ISY firmware, and is the most reliable + way to determine a device's type. + """ + if not hasattr(node, "node_def_id") or node.node_def_id is None: + # Node doesn't have a node_def (pre 5.0 firmware most likely) + return False + + node_def_id = node.node_def_id + + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]: + hass_isy_data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_insteon_type( + hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None +) -> bool: + """Check if the node matches the Insteon type for any platforms. + + This is for (presumably) every version of the ISY firmware, but only + works for Insteon device. "Node Server" (v5+) and Z-Wave and others will + not have a type. + """ + if not hasattr(node, "protocol") or node.protocol != PROTO_INSTEON: + return False + if not hasattr(node, "type") or node.type is None: + # Node doesn't have a type (non-Insteon device most likely) + return False + + device_type = node.type + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if any( + [ + device_type.startswith(t) + for t in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE]) + ] + ): + + # Hacky special-cases for certain devices with different platforms + # included as subnodes. Note that special-cases are not necessary + # on ISY 5.x firmware as it uses the superior NodeDefs method + subnode_id = int(node.address.split(" ")[-1], 16) + + # FanLinc, which has a light module as one of its nodes. + if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT: + hass_isy_data[ISY994_NODES][LIGHT].append(node) + return True + + # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 + if platform == CLIMATE and subnode_id in [ + SUBNODE_CLIMATE_COOL, + SUBNODE_CLIMATE_HEAT, + ]: + hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) + return True + + # IOLincs which have a sensor and relay on 2 different nodes + if ( + platform == BINARY_SENSOR + and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) + and subnode_id == SUBNODE_IOLINC_RELAY + ): + hass_isy_data[ISY994_NODES][SWITCH].append(node) + return True + + # Smartenit EZIO2X4 + if ( + platform == SWITCH + and device_type.startswith(TYPE_EZIO2X4) + and subnode_id in SUBNODE_EZIO2X4_SENSORS + ): + hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) + return True + + hass_isy_data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_zwave_cat( + hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None +) -> bool: + """Check if the node matches the ISY Z-Wave Category for any platforms. + + This is for (presumably) every version of the ISY firmware, but only + works for Z-Wave Devices with the devtype.cat property. + """ + if not hasattr(node, "protocol") or node.protocol != PROTO_ZWAVE: + return False + + if not hasattr(node, "zwave_props") or node.zwave_props is None: + # Node doesn't have a device type category (non-Z-Wave device) + return False + + device_type = node.zwave_props.category + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if any( + [ + device_type.startswith(t) + for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) + ] + ): + + hass_isy_data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_uom_id( + hass_isy_data: dict, + node: Union[Group, Node], + single_platform: str = None, + uom_list: list = None, +) -> bool: + """Check if a node's uom matches any of the platforms uom filter. + + This is used for versions of the ISY firmware that report uoms as a single + ID. We can often infer what type of device it is by that ID. + """ + if not hasattr(node, "uom") or node.uom in [None, ""]: + # Node doesn't have a uom (Scenes for example) + return False + + # Backwards compatibility for ISYv4 Firmware: + node_uom = node.uom + if isinstance(node.uom, list): + node_uom = node.uom[0] + + if uom_list: + if node_uom in uom_list: + hass_isy_data[ISY994_NODES][single_platform].append(node) + return True + return False + + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom in NODE_FILTERS[platform][FILTER_UOM]: + hass_isy_data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_states_in_uom( + hass_isy_data: dict, + node: Union[Group, Node], + single_platform: str = None, + states_list: list = None, +) -> bool: + """Check if a list of uoms matches two possible filters. + + This is for versions of the ISY firmware that report uoms as a list of all + possible "human readable" states. This filter passes if all of the possible + states fit inside the given filter. + """ + if not hasattr(node, "uom") or node.uom in [None, ""]: + # Node doesn't have a uom (Scenes for example) + return False + + # This only works for ISYv4 Firmware where uom is a list of states: + if not isinstance(node.uom, list): + return False + + node_uom = set(map(str.lower, node.uom)) + + if states_list: + if node_uom == set(states_list): + hass_isy_data[ISY994_NODES][single_platform].append(node) + return True + return False + + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]): + hass_isy_data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Union[Group, Node]) -> bool: + """Determine if the given sensor node should be a binary_sensor.""" + if _check_for_node_def(hass_isy_data, node, single_platform=BINARY_SENSOR): + return True + if _check_for_insteon_type(hass_isy_data, node, single_platform=BINARY_SENSOR): + return True + + # For the next two checks, we're providing our own set of uoms that + # represent on/off devices. This is because we can only depend on these + # checks in the context of already knowing that this is definitely a + # sensor device. + if _check_for_uom_id( + hass_isy_data, node, single_platform=BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS + ): + return True + if _check_for_states_in_uom( + hass_isy_data, + node, + single_platform=BINARY_SENSOR, + states_list=BINARY_SENSOR_ISY_STATES, + ): + return True + + return False + + +def _categorize_nodes( + hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str +) -> None: + """Sort the nodes to their proper platforms.""" + for (path, node) in nodes: + ignored = ignore_identifier in path or ignore_identifier in node.name + if ignored: + # Don't import this node as a device at all + continue + + if hasattr(node, "protocol") and node.protocol == PROTO_GROUP: + hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) + continue + + if sensor_identifier in path or sensor_identifier in node.name: + # User has specified to treat this as a sensor. First we need to + # determine if it should be a binary_sensor. + if _is_sensor_a_binary_sensor(hass_isy_data, node): + continue + hass_isy_data[ISY994_NODES][SENSOR].append(node) + continue + + # We have a bunch of different methods for determining the device type, + # each of which works with different ISY firmware versions or device + # family. The order here is important, from most reliable to least. + if _check_for_node_def(hass_isy_data, node): + continue + if _check_for_insteon_type(hass_isy_data, node): + continue + if _check_for_zwave_cat(hass_isy_data, node): + continue + if _check_for_uom_id(hass_isy_data, node): + continue + if _check_for_states_in_uom(hass_isy_data, node): + continue + + # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. + hass_isy_data[ISY994_NODES][SENSOR].append(node) + + +def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: + """Categorize the ISY994 programs.""" + for platform in SUPPORTED_PROGRAM_PLATFORMS: + folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") + if not folder: + continue + + for dtype, _, node_id in folder.children: + if dtype != TAG_FOLDER: + continue + entity_folder = folder[node_id] + + actions = None + status = entity_folder.get_by_name(KEY_STATUS) + if not status or not status.protocol == PROTO_PROGRAM: + _LOGGER.warning( + "Program %s entity '%s' not loaded, invalid/missing status program.", + platform, + entity_folder.name, + ) + continue + + if platform != BINARY_SENSOR: + actions = entity_folder.get_by_name(KEY_ACTIONS) + if not actions or not actions.protocol == PROTO_PROGRAM: + _LOGGER.warning( + "Program %s entity '%s' not loaded, invalid/missing actions program.", + platform, + entity_folder.name, + ) + continue + + entity = (entity_folder.name, status, actions) + hass_isy_data[ISY994_PROGRAMS][platform].append(entity) + + +def _categorize_variables( + hass_isy_data: dict, variables: Variables, identifier: str +) -> None: + """Gather the ISY994 Variables to be added as sensors.""" + try: + var_to_add = [ + (vtype, vname, vid) + for (vtype, vname, vid) in variables.children + if identifier in vname + ] + except KeyError as err: + _LOGGER.error("Error adding ISY Variables: %s", err) + return + for vtype, vname, vid in var_to_add: + hass_isy_data[ISY994_VARIABLES].append((vname, variables[vtype][vid])) + + +async def migrate_old_unique_ids( + hass: HomeAssistantType, platform: str, devices: Optional[List[Any]] +) -> None: + """Migrate to new controller-specific unique ids.""" + registry = await async_get_registry(hass) + + for device in devices: + old_entity_id = registry.async_get_entity_id( + platform, DOMAIN, device.old_unique_id + ) + if old_entity_id is not None: + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + device.old_unique_id, + device.unique_id, + ) + registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id) + + old_entity_id_2 = registry.async_get_entity_id( + platform, DOMAIN, device.unique_id.replace(":", "") + ) + if old_entity_id_2 is not None: + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + device.unique_id.replace(":", ""), + device.unique_id, + ) + registry.async_update_entity( + old_entity_id_2, new_unique_id=device.unique_id + ) + + +def convert_isy_value_to_hass( + value: Union[int, float, None], + uom: str, + precision: str, + fallback_precision: Optional[int] = None, +) -> Union[float, int]: + """Fix ISY Reported Values. + + ISY provides float values as an integer and precision component. + Correct by shifting the decimal place left by the value of precision. + (e.g. value=2345, prec="2" == 23.45) + + Insteon Thermostats report temperature in 0.5-deg precision as an int + by sending a value of 2 times the Temp. Correct by dividing by 2 here. + """ + if value is None or value == ISY_VALUE_UNKNOWN: + return None + if uom in [UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES]: + return round(float(value) / 2.0, 1) + if precision != "0": + return round(float(value) / 10 ** int(precision), int(precision)) + if fallback_precision: + return round(float(value), fallback_precision) + return value diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 39392ae062a..8d58a7f5796 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,79 +1,105 @@ """Support for ISY994 lights.""" -import logging -from typing import Callable +from typing import Callable, Dict -from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, Light +from pyisy.constants import ISY_VALUE_UNKNOWN + +from homeassistant.components.light import ( + DOMAIN as LIGHT, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import HomeAssistantType -from . import ISY994_NODES, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from .const import ( + _LOGGER, + CONF_RESTORE_LIGHT_STATE, + DOMAIN as ISY994_DOMAIN, + ISY994_NODES, +) +from .entity import ISYNodeEntity +from .helpers import migrate_old_unique_ids +from .services import async_setup_device_services, async_setup_light_services ATTR_LAST_BRIGHTNESS = "last_brightness" -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 light platform.""" + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + isy_options = entry.options + restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) + devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYLightDevice(node)) + for node in hass_isy_data[ISY994_NODES][LIGHT]: + devices.append(ISYLightEntity(node, restore_light_state)) - add_entities(devices) + await migrate_old_unique_ids(hass, LIGHT, devices) + async_add_entities(devices) + async_setup_device_services(hass) + async_setup_light_services(hass) -class ISYLightDevice(ISYDevice, Light, RestoreEntity): +class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): """Representation of an ISY994 light device.""" - def __init__(self, node) -> None: + def __init__(self, node, restore_light_state) -> None: """Initialize the ISY994 light device.""" super().__init__(node) self._last_brightness = None + self._restore_light_state = restore_light_state @property def is_on(self) -> bool: """Get whether the ISY994 light is on.""" - if self.is_unknown(): + if self._node.status == ISY_VALUE_UNKNOWN: return False - return self.value != 0 + return int(self._node.status) != 0 @property def brightness(self) -> float: """Get the brightness of the ISY994 light.""" - return None if self.is_unknown() else self.value + if self._node.status == ISY_VALUE_UNKNOWN: + return None + return int(self._node.status) def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" self._last_brightness = self.brightness - if not self._node.off(): + if not self._node.turn_off(): _LOGGER.debug("Unable to turn off light") def on_update(self, event: object) -> None: """Save brightness in the update event from the ISY994 Node.""" - if not self.is_unknown() and self.value != 0: - self._last_brightness = self.value + if self._node.status not in (0, ISY_VALUE_UNKNOWN): + self._last_brightness = self._node.status super().on_update(event) # pylint: disable=arguments-differ def turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" - if brightness is None and self._last_brightness: + if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness - if not self._node.on(val=brightness): + if not self._node.turn_on(val=brightness): _LOGGER.debug("Unable to turn on light") + @property + def device_state_attributes(self) -> Dict: + """Return the light attributes.""" + attribs = super().device_state_attributes + attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness + return attribs + @property def supported_features(self): """Flag supported features.""" return SUPPORT_BRIGHTNESS - @property - def device_state_attributes(self): - """Return the light attributes.""" - return {ATTR_LAST_BRIGHTNESS: self._last_brightness} - async def async_added_to_hass(self) -> None: """Restore last_brightness on restart.""" await super().async_added_to_hass() @@ -88,3 +114,11 @@ class ISYLightDevice(ISYDevice, Light, RestoreEntity): and last_state.attributes[ATTR_LAST_BRIGHTNESS] ): self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] + + def set_on_level(self, value): + """Set the ON Level for a device.""" + self._node.set_on_level(value) + + def set_ramp_rate(self, value): + """Set the Ramp Rate for a device.""" + self._node.set_ramp_rate(value) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 9ea7c9e1f6e..da50e4e704a 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,100 +1,74 @@ """Support for ISY994 locks.""" -import logging from typing import Callable -from homeassistant.components.lock import DOMAIN, LockDevice -from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED -from homeassistant.helpers.typing import ConfigType +from pyisy.constants import ISY_VALUE_UNKNOWN -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice +from homeassistant.components.lock import DOMAIN as LOCK, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .entity import ISYNodeEntity, ISYProgramEntity +from .helpers import migrate_old_unique_ids +from .services import async_setup_device_services -VALUE_TO_STATE = {0: STATE_UNLOCKED, 100: STATE_LOCKED} +VALUE_TO_STATE = {0: False, 100: True} -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 lock platform.""" + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYLockDevice(node)) + for node in hass_isy_data[ISY994_NODES][LOCK]: + devices.append(ISYLockEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYLockProgram(name, status, actions)) + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][LOCK]: + devices.append(ISYLockProgramEntity(name, status, actions)) - add_entities(devices) + await migrate_old_unique_ids(hass, LOCK, devices) + async_add_entities(devices) + async_setup_device_services(hass) -class ISYLockDevice(ISYDevice, LockDevice): +class ISYLockEntity(ISYNodeEntity, LockEntity): """Representation of an ISY994 lock device.""" - def __init__(self, node) -> None: - """Initialize the ISY994 lock device.""" - super().__init__(node) - self._conn = node.parent.parent.conn - @property def is_locked(self) -> bool: """Get whether the lock is in locked state.""" - return self.state == STATE_LOCKED - - @property - def state(self) -> str: - """Get the state of the lock.""" - if self.is_unknown(): + if self._node.status == ISY_VALUE_UNKNOWN: return None - return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN) + return VALUE_TO_STATE.get(self._node.status) def lock(self, **kwargs) -> None: """Send the lock command to the ISY994 device.""" - # Hack until PyISY is updated - req_url = self._conn.compileURL(["nodes", self.unique_id, "cmd", "SECMD", "1"]) - response = self._conn.request(req_url) - - if response is None: + if not self._node.secure_lock(): _LOGGER.error("Unable to lock device") - self._node.update(0.5) - def unlock(self, **kwargs) -> None: """Send the unlock command to the ISY994 device.""" - # Hack until PyISY is updated - req_url = self._conn.compileURL(["nodes", self.unique_id, "cmd", "SECMD", "0"]) - response = self._conn.request(req_url) - - if response is None: + if not self._node.secure_unlock(): _LOGGER.error("Unable to lock device") - self._node.update(0.5) - -class ISYLockProgram(ISYLockDevice): +class ISYLockProgramEntity(ISYProgramEntity, LockEntity): """Representation of a ISY lock program.""" - def __init__(self, name: str, node, actions) -> None: - """Initialize the lock.""" - super().__init__(node) - self._name = name - self._actions = actions - @property def is_locked(self) -> bool: """Return true if the device is locked.""" - return bool(self.value) - - @property - def state(self) -> str: - """Return the state of the lock.""" - return STATE_LOCKED if self.is_locked else STATE_UNLOCKED + return bool(self._node.status) def lock(self, **kwargs) -> None: """Lock the device.""" - if not self._actions.runThen(): + if not self._actions.run_then(): _LOGGER.error("Unable to lock device") def unlock(self, **kwargs) -> None: """Unlock the device.""" - if not self._actions.runElse(): + if not self._actions.run_else(): _LOGGER.error("Unable to unlock device") diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 083f25808fb..2effed0c06c 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,6 +2,13 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["PyISY==1.1.2"], - "codeowners": ["@bdraco"] + "requirements": ["pyisy==2.0.2"], + "codeowners": ["@bdraco", "@shbatm"], + "config_flow": true, + "ssdp": [ + { + "manufacturer": "Universal Devices Inc.", + "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1" + } + ] } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 1252d0ef53b..8ae646b2791 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,349 +1,121 @@ """Support for ISY994 sensors.""" -import logging -from typing import Callable +from typing import Callable, Dict, Union -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - DEGREE, - FREQUENCY_HERTZ, - LENGTH_CENTIMETERS, - LENGTH_KILOMETERS, - LENGTH_METERS, - MASS_KILOGRAMS, - POWER_WATT, - SPEED_KILOMETERS_PER_HOUR, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TIME_DAYS, - TIME_HOURS, - TIME_MILLISECONDS, - TIME_MINUTES, - TIME_MONTHS, - TIME_SECONDS, - TIME_YEARS, - UNIT_PERCENTAGE, - UV_INDEX, - VOLT, +from pyisy.constants import ISY_VALUE_UNKNOWN + +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + _LOGGER, + DOMAIN as ISY994_DOMAIN, + ISY994_NODES, + ISY994_VARIABLES, + UOM_DOUBLE_TEMP, + UOM_FRIENDLY_NAME, + UOM_TO_STATES, ) -from homeassistant.helpers.typing import ConfigType - -from . import ISY994_NODES, ISY994_WEATHER, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -UOM_FRIENDLY_NAME = { - "1": "amp", - "3": f"btu/{TIME_HOURS}", - "4": TEMP_CELSIUS, - "5": LENGTH_CENTIMETERS, - "6": "ft³", - "7": f"ft³/{TIME_MINUTES}", - "8": "m³", - "9": TIME_DAYS, - "10": TIME_DAYS, - "12": "dB", - "13": "dB A", - "14": DEGREE, - "16": "macroseismic", - "17": TEMP_FAHRENHEIT, - "18": "ft", - "19": TIME_HOURS, - "20": TIME_HOURS, - "21": "abs. humidity (%)", - "22": "rel. humidity (%)", - "23": "inHg", - "24": "in/hr", - "25": "index", - "26": "K", - "27": "keyword", - "28": MASS_KILOGRAMS, - "29": "kV", - "30": "kW", - "31": "kPa", - "32": SPEED_KILOMETERS_PER_HOUR, - "33": "kWH", - "34": "liedu", - "35": "l", - "36": "lx", - "37": "mercalli", - "38": LENGTH_METERS, - "39": "m³/hr", - "40": SPEED_METERS_PER_SECOND, - "41": "mA", - "42": TIME_MILLISECONDS, - "43": "mV", - "44": TIME_MINUTES, - "45": TIME_MINUTES, - "46": "mm/hr", - "47": TIME_MONTHS, - "48": SPEED_MILES_PER_HOUR, - "49": SPEED_METERS_PER_SECOND, - "50": "ohm", - "51": UNIT_PERCENTAGE, - "52": "lb", - "53": "power factor", - "54": CONCENTRATION_PARTS_PER_MILLION, - "55": "pulse count", - "57": TIME_SECONDS, - "58": TIME_SECONDS, - "59": "seimens/m", - "60": "body wave magnitude scale", - "61": "Ricter scale", - "62": "moment magnitude scale", - "63": "surface wave magnitude scale", - "64": "shindo", - "65": "SML", - "69": "gal", - "71": UV_INDEX, - "72": VOLT, - "73": POWER_WATT, - "74": f"{POWER_WATT}/m²", - "75": "weekday", - "76": f"Wind Direction ({DEGREE})", - "77": TIME_YEARS, - "82": "mm", - "83": LENGTH_KILOMETERS, - "85": "ohm", - "86": "kOhm", - "87": "m³/m³", - "88": "Water activity", - "89": "RPM", - "90": FREQUENCY_HERTZ, - "91": f"{DEGREE} (Relative to North)", - "92": f"{DEGREE} (Relative to South)", -} - -UOM_TO_STATES = { - "11": {"0": "unlocked", "100": "locked", "102": "jammed"}, - "15": { - "1": "master code changed", - "2": "tamper code entry limit", - "3": "escutcheon removed", - "4": "key/manually locked", - "5": "locked by touch", - "6": "key/manually unlocked", - "7": "remote locking jammed bolt", - "8": "remotely locked", - "9": "remotely unlocked", - "10": "deadbolt jammed", - "11": "battery too low to operate", - "12": "critical low battery", - "13": "low battery", - "14": "automatically locked", - "15": "automatic locking jammed bolt", - "16": "remotely power cycled", - "17": "lock handling complete", - "19": "user deleted", - "20": "user added", - "21": "duplicate pin", - "22": "jammed bolt by locking with keypad", - "23": "locked by keypad", - "24": "unlocked by keypad", - "25": "keypad attempt outside schedule", - "26": "hardware failure", - "27": "factory reset", - }, - "66": { - "0": "idle", - "1": "heating", - "2": "cooling", - "3": "fan only", - "4": "pending heat", - "5": "pending cool", - "6": "vent", - "7": "aux heat", - "8": "2nd stage heating", - "9": "2nd stage cooling", - "10": "2nd stage aux heat", - "11": "3rd stage aux heat", - }, - "67": { - "0": "off", - "1": "heat", - "2": "cool", - "3": "auto", - "4": "aux/emergency heat", - "5": "resume", - "6": "fan only", - "7": "furnace", - "8": "dry air", - "9": "moist air", - "10": "auto changeover", - "11": "energy save heat", - "12": "energy save cool", - "13": "away", - }, - "68": { - "0": "auto", - "1": "on", - "2": "auto high", - "3": "high", - "4": "auto medium", - "5": "medium", - "6": "circulation", - "7": "humidity circulation", - }, - "93": { - "1": "power applied", - "2": "ac mains disconnected", - "3": "ac mains reconnected", - "4": "surge detection", - "5": "volt drop or drift", - "6": "over current detected", - "7": "over voltage detected", - "8": "over load detected", - "9": "load error", - "10": "replace battery soon", - "11": "replace battery now", - "12": "battery is charging", - "13": "battery is fully charged", - "14": "charge battery soon", - "15": "charge battery now", - }, - "94": { - "1": "program started", - "2": "program in progress", - "3": "program completed", - "4": "replace main filter", - "5": "failure to set target temperature", - "6": "supplying water", - "7": "water supply failure", - "8": "boiling", - "9": "boiling failure", - "10": "washing", - "11": "washing failure", - "12": "rinsing", - "13": "rinsing failure", - "14": "draining", - "15": "draining failure", - "16": "spinning", - "17": "spinning failure", - "18": "drying", - "19": "drying failure", - "20": "fan failure", - "21": "compressor failure", - }, - "95": { - "1": "leaving bed", - "2": "sitting on bed", - "3": "lying on bed", - "4": "posture changed", - "5": "sitting on edge of bed", - }, - "96": { - "1": "clean", - "2": "slightly polluted", - "3": "moderately polluted", - "4": "highly polluted", - }, - "97": { - "0": "closed", - "100": "open", - "102": "stopped", - "103": "closing", - "104": "opening", - }, -} +from .entity import ISYEntity, ISYNodeEntity +from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +from .services import async_setup_device_services -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 sensor platform.""" + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass_isy_data[ISY994_NODES][SENSOR]: _LOGGER.debug("Loading %s", node.name) - devices.append(ISYSensorDevice(node)) + devices.append(ISYSensorEntity(node)) - for node in hass.data[ISY994_WEATHER]: - devices.append(ISYWeatherDevice(node)) + for vname, vobj in hass_isy_data[ISY994_VARIABLES]: + devices.append(ISYSensorVariableEntity(vname, vobj)) - add_entities(devices) + await migrate_old_unique_ids(hass, SENSOR, devices) + async_add_entities(devices) + async_setup_device_services(hass) -class ISYSensorDevice(ISYDevice): +class ISYSensorEntity(ISYNodeEntity): """Representation of an ISY994 sensor device.""" @property - def raw_unit_of_measurement(self) -> str: + def raw_unit_of_measurement(self) -> Union[dict, str]: """Get the raw unit of measurement for the ISY994 sensor device.""" - if len(self._node.uom) == 1: - if self._node.uom[0] in UOM_FRIENDLY_NAME: - friendly_name = UOM_FRIENDLY_NAME.get(self._node.uom[0]) - if friendly_name in (TEMP_CELSIUS, TEMP_FAHRENHEIT): - friendly_name = self.hass.config.units.temperature_unit - return friendly_name - return self._node.uom[0] - return None + uom = self._node.uom + + # Backwards compatibility for ISYv4 Firmware: + if isinstance(uom, list): + return UOM_FRIENDLY_NAME.get(uom[0], uom[0]) + + # Special cases for ISY UOM index units: + isy_states = UOM_TO_STATES.get(uom) + if isy_states: + return isy_states + + return UOM_FRIENDLY_NAME.get(uom) @property def state(self) -> str: """Get the state of the ISY994 sensor device.""" - if self.is_unknown(): + value = self._node.status + if value == ISY_VALUE_UNKNOWN: return None - if len(self._node.uom) == 1: - if self._node.uom[0] in UOM_TO_STATES: - states = UOM_TO_STATES.get(self._node.uom[0]) - if self.value in states: - return states.get(self.value) - elif self._node.prec and self._node.prec != [0]: - str_val = str(self.value) - int_prec = int(self._node.prec) - decimal_part = str_val[-int_prec:] - whole_part = str_val[: len(str_val) - int_prec] - val = float(f"{whole_part}.{decimal_part}") - raw_units = self.raw_unit_of_measurement - if raw_units in (TEMP_CELSIUS, TEMP_FAHRENHEIT): - val = self.hass.config.units.temperature(val, raw_units) + # Get the translated ISY Unit of Measurement + uom = self.raw_unit_of_measurement - return str(val) - else: - return self.value + # Check if this is a known index pair UOM + if isinstance(uom, dict): + return uom.get(value, value) - return None + # Handle ISY precision and rounding + value = convert_isy_value_to_hass(value, uom, self._node.prec) + + # Convert temperatures to Home Assistant's unit + if uom in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + value = self.hass.config.units.temperature(value, uom) + + return value @property def unit_of_measurement(self) -> str: - """Get the unit of measurement for the ISY994 sensor device.""" + """Get the Home Assistant unit of measurement for the device.""" raw_units = self.raw_unit_of_measurement - if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS): + # Check if this is a known index pair UOM + if isinstance(raw_units, dict): + return None + if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS, UOM_DOUBLE_TEMP): return self.hass.config.units.temperature_unit return raw_units -class ISYWeatherDevice(ISYDevice): - """Representation of an ISY994 weather device.""" +class ISYSensorVariableEntity(ISYEntity): + """Representation of an ISY994 variable as a sensor device.""" + + def __init__(self, vname: str, vobj: object) -> None: + """Initialize the ISY994 binary sensor program.""" + super().__init__(vobj) + self._name = vname @property - def raw_units(self) -> str: - """Return the raw unit of measurement.""" - if self._node.uom == "F": - return TEMP_FAHRENHEIT - if self._node.uom == "C": - return TEMP_CELSIUS - return self._node.uom + def state(self): + """Return the state of the variable.""" + return self._node.status @property - def state(self) -> object: - """Return the value of the node.""" - # pylint: disable=protected-access - val = self._node.status._val - raw_units = self._node.uom - - if raw_units in [TEMP_CELSIUS, TEMP_FAHRENHEIT]: - return self.hass.config.units.temperature(val, raw_units) - return val + def device_state_attributes(self) -> Dict: + """Get the state attributes for the device.""" + return {"init_value": int(self._node.init)} @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement for the node.""" - raw_units = self.raw_units - - if raw_units in [TEMP_CELSIUS, TEMP_FAHRENHEIT]: - return self.hass.config.units.temperature_unit - return raw_units + def icon(self): + """Return the icon.""" + return "mdi:counter" diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py new file mode 100644 index 00000000000..f9004ecdfef --- /dev/null +++ b/homeassistant/components/isy994/services.py @@ -0,0 +1,408 @@ +"""ISY Services and Commands.""" + +from typing import Any + +from pyisy.constants import COMMAND_FRIENDLY_NAME +import voluptuous as vol + +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND, + CONF_NAME, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + SERVICE_RELOAD, +) +from homeassistant.core import callback +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + _LOGGER, + DOMAIN, + ISY994_ISY, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_VARIABLES, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, +) + +# Common Services for All Platforms: +SERVICE_SYSTEM_QUERY = "system_query" +SERVICE_SET_VARIABLE = "set_variable" +SERVICE_SEND_PROGRAM_COMMAND = "send_program_command" +SERVICE_RUN_NETWORK_RESOURCE = "run_network_resource" +SERVICE_CLEANUP = "cleanup_entities" + +INTEGRATION_SERVICES = [ + SERVICE_SYSTEM_QUERY, + SERVICE_SET_VARIABLE, + SERVICE_SEND_PROGRAM_COMMAND, + SERVICE_RUN_NETWORK_RESOURCE, + SERVICE_CLEANUP, +] + +# Entity specific methods (valid for most Groups/ISY Scenes, Lights, Switches, Fans) +SERVICE_SEND_RAW_NODE_COMMAND = "send_raw_node_command" +SERVICE_SEND_NODE_COMMAND = "send_node_command" + +# Services valid only for dimmable lights. +SERVICE_SET_ON_LEVEL = "set_on_level" +SERVICE_SET_RAMP_RATE = "set_ramp_rate" + +CONF_PARAMETERS = "parameters" +CONF_VALUE = "value" +CONF_INIT = "init" +CONF_ISY = "isy" + +VALID_NODE_COMMANDS = [ + "beep", + "brighten", + "dim", + "disable", + "enable", + "fade_down", + "fade_stop", + "fade_up", + "fast_off", + "fast_on", + "query", +] +VALID_PROGRAM_COMMANDS = [ + "run", + "run_then", + "run_else", + "stop", + "enable", + "disable", + "enable_run_at_startup", + "disable_run_at_startup", +] + + +def valid_isy_commands(value: Any) -> str: + """Validate the command is valid.""" + value = str(value).upper() + if value in COMMAND_FRIENDLY_NAME.keys(): + return value + raise vol.Invalid("Invalid ISY Command.") + + +SCHEMA_GROUP = "name-address" + +SERVICE_SYSTEM_QUERY_SCHEMA = vol.Schema( + {vol.Optional(CONF_ADDRESS): cv.string, vol.Optional(CONF_ISY): cv.string} +) + +SERVICE_SET_RAMP_RATE_SCHEMA = { + vol.Required(CONF_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 31)) +} + +SERVICE_SET_VALUE_SCHEMA = { + vol.Required(CONF_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 255)) +} + +SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA = { + vol.Required(CONF_COMMAND): vol.All(cv.string, valid_isy_commands), + vol.Optional(CONF_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 255)), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(vol.Coerce(int), vol.Range(0, 120)), + vol.Optional(CONF_PARAMETERS, default={}): {cv.string: cv.string}, +} + +SERVICE_SEND_NODE_COMMAND_SCHEMA = { + vol.Required(CONF_COMMAND): vol.In(VALID_NODE_COMMANDS) +} + +SERVICE_SET_VARIABLE_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_ADDRESS, CONF_TYPE, CONF_NAME), + vol.Schema( + { + vol.Exclusive(CONF_NAME, SCHEMA_GROUP): cv.string, + vol.Inclusive(CONF_ADDRESS, SCHEMA_GROUP): vol.Coerce(int), + vol.Inclusive(CONF_TYPE, SCHEMA_GROUP): vol.All( + vol.Coerce(int), vol.Range(1, 2) + ), + vol.Optional(CONF_INIT, default=False): bool, + vol.Required(CONF_VALUE): vol.Coerce(int), + vol.Optional(CONF_ISY): cv.string, + } + ), +) + +SERVICE_SEND_PROGRAM_COMMAND_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_ADDRESS, CONF_NAME), + vol.Schema( + { + vol.Exclusive(CONF_NAME, SCHEMA_GROUP): cv.string, + vol.Exclusive(CONF_ADDRESS, SCHEMA_GROUP): cv.string, + vol.Required(CONF_COMMAND): vol.In(VALID_PROGRAM_COMMANDS), + vol.Optional(CONF_ISY): cv.string, + } + ), +) + +SERVICE_RUN_NETWORK_RESOURCE_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_ADDRESS, CONF_NAME), + vol.Schema( + { + vol.Exclusive(CONF_NAME, SCHEMA_GROUP): cv.string, + vol.Exclusive(CONF_ADDRESS, SCHEMA_GROUP): vol.Coerce(int), + vol.Optional(CONF_ISY): cv.string, + } + ), +) + + +@callback +def async_setup_services(hass: HomeAssistantType): + """Create and register services for the ISY integration.""" + existing_services = hass.services.async_services().get(DOMAIN) + if existing_services and any( + service in INTEGRATION_SERVICES for service in existing_services.keys() + ): + # Integration-level services have already been added. Return. + return + + async def async_system_query_service_handler(service): + """Handle a system query service call.""" + address = service.data.get(CONF_ADDRESS) + isy_name = service.data.get(CONF_ISY) + + for config_entry_id in hass.data[DOMAIN]: + isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + if isy_name and not isy_name == isy.configuration["name"]: + continue + # If an address is provided, make sure we query the correct ISY. + # Otherwise, query the whole system on all ISY's connected. + if address and isy.nodes.get_by_id(address) is not None: + _LOGGER.debug( + "Requesting query of device %s on ISY %s", + address, + isy.configuration["uuid"], + ) + await hass.async_add_executor_job(isy.query, address) + return + _LOGGER.debug( + "Requesting system query of ISY %s", isy.configuration["uuid"] + ) + await hass.async_add_executor_job(isy.query) + + async def async_run_network_resource_service_handler(service): + """Handle a network resource service call.""" + address = service.data.get(CONF_ADDRESS) + name = service.data.get(CONF_NAME) + isy_name = service.data.get(CONF_ISY) + + for config_entry_id in hass.data[DOMAIN]: + isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + if isy_name and not isy_name == isy.configuration["name"]: + continue + if not hasattr(isy, "networking") or isy.networking is None: + continue + command = None + if address: + command = isy.networking.get_by_id(address) + if name: + command = isy.networking.get_by_name(name) + if command is not None: + await hass.async_add_executor_job(command.run) + return + _LOGGER.error( + "Could not run network resource command. Not found or enabled on the ISY." + ) + + async def async_send_program_command_service_handler(service): + """Handle a send program command service call.""" + address = service.data.get(CONF_ADDRESS) + name = service.data.get(CONF_NAME) + command = service.data.get(CONF_COMMAND) + isy_name = service.data.get(CONF_ISY) + + for config_entry_id in hass.data[DOMAIN]: + isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + if isy_name and not isy_name == isy.configuration["name"]: + continue + program = None + if address: + program = isy.programs.get_by_id(address) + if name: + program = isy.programs.get_by_name(name) + if program is not None: + await hass.async_add_executor_job(getattr(program, command)) + return + _LOGGER.error( + "Could not send program command. Not found or enabled on the ISY." + ) + + async def async_set_variable_service_handler(service): + """Handle a set variable service call.""" + address = service.data.get(CONF_ADDRESS) + vtype = service.data.get(CONF_TYPE) + name = service.data.get(CONF_NAME) + value = service.data.get(CONF_VALUE) + init = service.data.get(CONF_INIT, False) + isy_name = service.data.get(CONF_ISY) + + for config_entry_id in hass.data[DOMAIN]: + isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + if isy_name and not isy_name == isy.configuration["name"]: + continue + variable = None + if name: + variable = isy.variables.get_by_name(name) + if address and vtype: + variable = isy.variables.vobjs[vtype].get(address) + if variable is not None: + await hass.async_add_executor_job(variable.set_value, value, init) + return + _LOGGER.error("Could not set variable value. Not found or enabled on the ISY.") + + async def async_cleanup_registry_entries(service) -> None: + """Remove extra entities that are no longer part of the integration.""" + entity_registry = await er.async_get_registry(hass) + config_ids = [] + current_unique_ids = [] + + for config_entry_id in hass.data[DOMAIN]: + entries_for_this_config = er.async_entries_for_config_entry( + entity_registry, config_entry_id + ) + config_ids.extend( + [ + (entity.unique_id, entity.entity_id) + for entity in entries_for_this_config + ] + ) + + hass_isy_data = hass.data[DOMAIN][config_entry_id] + uuid = hass_isy_data[ISY994_ISY].configuration["uuid"] + + for platform in SUPPORTED_PLATFORMS: + for node in hass_isy_data[ISY994_NODES][platform]: + if hasattr(node, "address"): + current_unique_ids.append(f"{uuid}_{node.address}") + + for platform in SUPPORTED_PROGRAM_PLATFORMS: + for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]: + if hasattr(node, "address"): + current_unique_ids.append(f"{uuid}_{node.address}") + + for node in hass_isy_data[ISY994_VARIABLES]: + if hasattr(node, "address"): + current_unique_ids.append(f"{uuid}_{node.address}") + + extra_entities = [ + entity_id + for unique_id, entity_id in config_ids + if unique_id not in current_unique_ids + ] + + for entity_id in extra_entities: + if entity_registry.async_is_registered(entity_id): + entity_registry.async_remove(entity_id) + + _LOGGER.debug( + "Cleaning up ISY994 Entities and devices: Config Entries: %s, Current Entries: %s, " + "Extra Entries Removed: %s", + len(config_ids), + len(current_unique_ids), + len(extra_entities), + ) + + async def async_reload_config_entries(service) -> None: + """Trigger a reload of all ISY994 config entries.""" + for config_entry_id in hass.data[DOMAIN]: + hass.async_create_task(hass.config_entries.async_reload(config_entry_id)) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SYSTEM_QUERY, + service_func=async_system_query_service_handler, + schema=SERVICE_SYSTEM_QUERY_SCHEMA, + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_RUN_NETWORK_RESOURCE, + service_func=async_run_network_resource_service_handler, + schema=SERVICE_RUN_NETWORK_RESOURCE_SCHEMA, + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SEND_PROGRAM_COMMAND, + service_func=async_send_program_command_service_handler, + schema=SERVICE_SEND_PROGRAM_COMMAND_SCHEMA, + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_VARIABLE, + service_func=async_set_variable_service_handler, + schema=SERVICE_SET_VARIABLE_SCHEMA, + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_CLEANUP, + service_func=async_cleanup_registry_entries, + ) + + hass.services.async_register( + domain=DOMAIN, service=SERVICE_RELOAD, service_func=async_reload_config_entries + ) + + +@callback +def async_unload_services(hass: HomeAssistantType): + """Unload services for the ISY integration.""" + if hass.data[DOMAIN]: + # There is still another config entry for this domain, don't remove services. + return + + existing_services = hass.services.async_services().get(DOMAIN) + if not existing_services or not any( + service in INTEGRATION_SERVICES for service in existing_services.keys() + ): + return + + _LOGGER.info("Unloading ISY994 Services.") + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SYSTEM_QUERY) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_RUN_NETWORK_RESOURCE) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_PROGRAM_COMMAND) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_VARIABLE) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_CLEANUP) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_RELOAD) + + +@callback +def async_setup_device_services(hass: HomeAssistantType): + """Create device-specific services for the ISY Integration.""" + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SEND_RAW_NODE_COMMAND, + SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA, + SERVICE_SEND_RAW_NODE_COMMAND, + ) + platform.async_register_entity_service( + SERVICE_SEND_NODE_COMMAND, + SERVICE_SEND_NODE_COMMAND_SCHEMA, + SERVICE_SEND_NODE_COMMAND, + ) + + +@callback +def async_setup_light_services(hass: HomeAssistantType): + """Create device-specific services for the ISY Integration.""" + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_ON_LEVEL, SERVICE_SET_VALUE_SCHEMA, SERVICE_SET_ON_LEVEL + ) + platform.async_register_entity_service( + SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, SERVICE_SET_RAMP_RATE + ) diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml new file mode 100644 index 00000000000..04fc04083d5 --- /dev/null +++ b/homeassistant/components/isy994/services.yaml @@ -0,0 +1,115 @@ +# Describes the ISY994-specific services available + +# Note: controlling many entity_ids with one call is not recommended since it may result in +# flooding the ISY with requests. To control multiple devices with a service call +# the recommendation is to add a scene in the ISY and control that scene. +send_raw_node_command: + description: Send a "raw" ISY REST Device Command to a Node using its Home Assistant Entity ID. + fields: + entity_id: + description: Name of an entity to send command. + example: "light.front_door" + command: + description: The ISY REST Command to be sent to the device + example: "DON" + value: + description: (Optional) The integer value to be sent with the command. + example: 255 + parameters: + description: (Optional) A dict of parameters to be sent in the query string (e.g. for controlling colored bulbs). + example: { GV2: 0, GV3: 0, GV4: 255 } + unit_of_measurement: + description: (Optional) The ISY Unit of Measurement (UOM) to send with the command, if required. + example: 67 +send_node_command: + description: >- + Send a command to an ISY Device using its Home Assistant entity ID. Valid commands are: beep, brighten, dim, disable, + enable, fade_down, fade_stop, fade_up, fast_off, fast_on, and query. + fields: + entity_id: + description: Name of an entity to send command. + example: "light.front_door" + command: + description: The command to be sent to the device. + example: "fast_on" +set_on_level: + description: Send a ISY set_on_level command to a Node. + fields: + entity_id: + description: Name of an entity to send command. + example: "light.front_door" + value: + description: integer value to set (0-255). + example: 255 +set_ramp_rate: + description: Send a ISY set_ramp_rate command to a Node. + fields: + entity_id: + description: Name of an entity to send command. + example: "light.front_door" + value: + description: Integer value to set (0-31), see PyISY/ISY documentation for values to actual ramp times. + example: 30 +system_query: + description: Request the ISY Query the connected devices. + fields: + address: + description: (Optional) ISY Address to Query. Omitting this requests a system-wide scan (typically scheduled once per day). + example: "1A 2B 3C 1" + isy: + description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). Omitting this will cause all ISYs to be queried. + example: "ISY" +set_variable: + description: Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. + fields: + address: + description: The address of the variable for which to set the value. + example: 5 + type: + description: The variable type, 1 = Integer, 2 = State. + example: 2 + name: + description: (Optional) The name of the variable to set (use instead of type/address). + example: "my_variable_name" + init: + description: (Optional) If True, the initial (init) value will be updated instead of the current value. + example: false + value: + description: The integer value to be sent. + example: 255 + isy: + description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same variable name or address on multiple ISYs, omitting this will run the command on them all. + example: "ISY" +send_program_command: + description: >- + Send a command to control an ISY program or folder. Valid commands are run, run_then, run_else, stop, enable, disable, + enable_run_at_startup, and disable_run_at_startup. + fields: + address: + description: The address of the program to control (optional, use either address or name). + example: "04B1" + name: + description: The name of the program to control (optional, use either address or name). + example: "My Program" + command: + description: The ISY Program Command to be sent. + example: "run" + isy: + description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all. + example: "ISY" +run_network_resource: + description: Run a network resource on the ISY. + fields: + address: + description: The address of the network resource to execute (optional, use either address or name). + example: 121 + name: + description: The name of the network resource to execute (optional, use either address or name). + example: "Network Resource 1" + isy: + description: (Optional) If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same resource name or address on multiple ISYs, omitting this will run the command on them all. + example: "ISY" +reload: + description: Reload the ISY994 connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. +cleanup_entities: + description: Cleanup old entities and devices no longer used by the ISY994 integrations. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json new file mode 100644 index 00000000000..ce9818bc0c8 --- /dev/null +++ b/homeassistant/components/isy994/strings.json @@ -0,0 +1,41 @@ +{ + "title": "Universal Devices ISY994", + "config": { + "flow_title": "Universal Devices ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "host": "URL", + "password": "[%key:common::config_flow::data::password%]", + "tls": "The TLS version of the ISY controller." + }, + "description": "The host entry must be in full URL format, e.g., http://192.168.10.100:80", + "title": "Connect to your ISY994" + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "title": "ISY994 Options", + "description": "Set the options for the ISY Integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", + "data": { + "sensor_string": "Node Sensor String", + "ignore_string": "Ignore String", + "variable_sensor_string": "Variable Sensor String", + "restore_light_state": "Restore Light Brightness" + } + } + } + } +} diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index aadd4428abd..0f79d3f218f 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,69 +1,84 @@ """Support for ISY994 switches.""" -import logging from typing import Callable -from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.helpers.typing import ConfigType +from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice +from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .entity import ISYNodeEntity, ISYProgramEntity +from .helpers import migrate_old_unique_ids +from .services import async_setup_device_services -def setup_platform( - hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None -): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[list], None], +) -> bool: """Set up the ISY994 switch platform.""" + hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - if not node.dimmable: - devices.append(ISYSwitchDevice(node)) + for node in hass_isy_data[ISY994_NODES][SWITCH]: + devices.append(ISYSwitchEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYSwitchProgram(name, status, actions)) + for name, status, actions in hass_isy_data[ISY994_PROGRAMS][SWITCH]: + devices.append(ISYSwitchProgramEntity(name, status, actions)) - add_entities(devices) + await migrate_old_unique_ids(hass, SWITCH, devices) + async_add_entities(devices) + async_setup_device_services(hass) -class ISYSwitchDevice(ISYDevice, SwitchDevice): +class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): """Representation of an ISY994 switch device.""" @property def is_on(self) -> bool: """Get whether the ISY994 device is in the on state.""" - return bool(self.value) + if self._node.status == ISY_VALUE_UNKNOWN: + return None + return bool(self._node.status) def turn_off(self, **kwargs) -> None: - """Send the turn on command to the ISY994 switch.""" - if not self._node.off(): - _LOGGER.debug("Unable to turn on switch.") + """Send the turn off command to the ISY994 switch.""" + if not self._node.turn_off(): + _LOGGER.debug("Unable to turn off switch.") def turn_on(self, **kwargs) -> None: - """Send the turn off command to the ISY994 switch.""" - if not self._node.on(): + """Send the turn on command to the ISY994 switch.""" + if not self._node.turn_on(): _LOGGER.debug("Unable to turn on switch.") + @property + def icon(self) -> str: + """Get the icon for groups.""" + if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: + return "mdi:google-circles-communities" # Matches isy scene icon + return super().icon -class ISYSwitchProgram(ISYSwitchDevice): + +class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): """A representation of an ISY994 program switch.""" - def __init__(self, name: str, node, actions) -> None: - """Initialize the ISY994 switch program.""" - super().__init__(node) - self._name = name - self._actions = actions - @property def is_on(self) -> bool: """Get whether the ISY994 switch program is on.""" - return bool(self.value) + return bool(self._node.status) def turn_on(self, **kwargs) -> None: """Send the turn on command to the ISY994 switch program.""" - if not self._actions.runThen(): + if not self._actions.run_then(): _LOGGER.error("Unable to turn on switch") def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 switch program.""" - if not self._actions.runElse(): + if not self._actions.run_else(): _LOGGER.error("Unable to turn off switch") + + @property + def icon(self) -> str: + """Get the icon for programs.""" + return "mdi:script-text-outline" # Matches isy program icon diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json new file mode 100644 index 00000000000..aa9c188f8dc --- /dev/null +++ b/homeassistant/components/isy994/translations/ca.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key::common::config_flow::error::invalid_auth%]", + "unknown": "Error inesperat" + }, + "flow_title": "Dispositius universals ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "host": "URL", + "password": "[%key::common::config_flow::data::password%]", + "tls": "Versi\u00f3 TLS del controlador ISY.", + "username": "[%key::common::config_flow::data::username%]" + }, + "title": "Connexi\u00f3 amb ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "String Ignora", + "restore_light_state": "Restaura la brillantor de la llum", + "sensor_string": "String de Sensor Node", + "variable_sensor_string": "String de Sensor Variable" + }, + "description": "Configuraci\u00f3 de les opcions per a la integraci\u00f3 ISY: \n \u2022 String de Sensor Node: qualsevol dispositiu o carpeta que contingui 'String de Sensor Node' dins el nom ser\u00e0 tractat com a un sensor o sensor binari. \n \u2022 String Ignora: qualsevol dispositiu amb 'String Ignora' dins el nom ser\u00e0 ignorat. \n \u2022 String de Sensor Variable: qualsevol variable que contingui 'String de Sensor Variable' s'afegir\u00e0 com a un sensor. \n \u2022 Restaura la brillantor de la llum: si est\u00e0 activat, en encendre un llum es restablir\u00e0 la brillantor anterior en lloc del valor integrat dins el dispositiu.", + "title": "Opcions d'ISY994" + } + } + }, + "title": "Dispositius universals ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json new file mode 100644 index 00000000000..a40ab60cd1e --- /dev/null +++ b/homeassistant/components/isy994/translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json new file mode 100644 index 00000000000..61fe07fd1fc --- /dev/null +++ b/homeassistant/components/isy994/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80", + "unknown": "Unexpected error" + }, + "flow_title": "Universal Devices ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "host": "URL", + "password": "Password", + "tls": "The TLS version of the ISY controller.", + "username": "Username" + }, + "description": "The host entry must be in full URL format, e.g., http://192.168.10.100:80", + "title": "Connect to your ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignore String", + "restore_light_state": "Restore Light Brightness", + "sensor_string": "Node Sensor String", + "variable_sensor_string": "Variable Sensor String" + }, + "description": "Set the options for the ISY Integration: \n \u2022 Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n \u2022 Ignore String: Any device with 'Ignore String' in the name will be ignored. \n \u2022 Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n \u2022 Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", + "title": "ISY994 Options" + } + } + }, + "title": "Universal Devices ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json new file mode 100644 index 00000000000..1edd249e9d7 --- /dev/null +++ b/homeassistant/components/isy994/translations/es.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "Contrase\u00f1a", + "tls": "La versi\u00f3n de TLS del controlador ISY.", + "username": "Usuario" + }, + "description": "La entrada del host debe estar en formato URL completo, por ejemplo, http://192.168.10.100:80", + "title": "Conectar con tu ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignorar Cadena", + "restore_light_state": "Restaurar Intensidad de la Luz", + "sensor_string": "Cadena Nodo Sensor", + "variable_sensor_string": "Cadena de Sensor Variable" + }, + "description": "Configura las opciones para la integraci\u00f3n de ISY: \n \u2022 Cadena Nodo Sensor: Cualquier dispositivo o carpeta que contenga 'Cadena Nodo Sensor' en el nombre ser\u00e1 tratada como un sensor o un sensor binario. \n \u2022 Ignorar Cadena: Cualquier dispositivo con 'Ignorar Cadena' en el nombre ser\u00e1 ignorado. \n \u2022 Restaurar Intensidad de la Luz: Si se habilita, la intensidad anterior ser\u00e1 restaurada al encender una luz en lugar de usar el nivel predeterminado del dispositivo.", + "title": "Opciones ISY994" + } + } + }, + "title": "Dispositivos Universales ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/fi.json b/homeassistant/components/isy994/translations/fi.json new file mode 100644 index 00000000000..c5b20ac47a4 --- /dev/null +++ b/homeassistant/components/isy994/translations/fi.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "invalid_host": "Palvelimen osoite ei ollut t\u00e4ydess\u00e4 URL-muodossa, esim. http://192.168.10.100:80" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "Salasana", + "tls": "ISY-ohjaimen TLS-versio.", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ohita merkkijono", + "restore_light_state": "Valon kirkkauden palauttaminen", + "variable_sensor_string": "Muuttuva anturin merkkijono" + }, + "title": "ISY994-asetukset" + } + } + }, + "title": "Universaalit laitteet ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json new file mode 100644 index 00000000000..d075764a7c5 --- /dev/null +++ b/homeassistant/components/isy994/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "URL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignorer la cha\u00eene" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/he.json b/homeassistant/components/isy994/translations/he.json new file mode 100644 index 00000000000..ed6f54fc696 --- /dev/null +++ b/homeassistant/components/isy994/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e2\u05e8\u05da \u05d4\u05beHost \u05dc\u05d0 \u05d4\u05d9\u05d4 \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 URL \u05de\u05dc\u05d0, \u05dc\u05de\u05e9\u05dc, http://192.168.10.100:80", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json new file mode 100644 index 00000000000..ccb5ff85b87 --- /dev/null +++ b/homeassistant/components/isy994/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_host": "La voce host non era nel formato URL completo, ad esempio http://192.168.10.100:80", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "Password", + "tls": "La versione TLS del controllore ISY.", + "username": "Nome utente" + }, + "description": "La voce host deve essere nel formato URL completo, ad esempio, http://192.168.10.100:80", + "title": "Connettersi a ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignora Stringa", + "restore_light_state": "Ripristina Luminosit\u00e0 Luce", + "sensor_string": "Stringa Nodo Sensore", + "variable_sensor_string": "Stringa Variabile Sensore" + }, + "description": "Impostare le opzioni per l'integrazione ISY: \n \u2022 Stringa Nodo Sensore: qualsiasi dispositivo o cartella che contiene \"Stringa Nodo Sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora Stringa: qualsiasi dispositivo con \"Ignora Stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa Variabile Sensore: qualsiasi variabile che contiene \"Stringa Variabile Sensore\" verr\u00e0 aggiunta come sensore. \n \u2022 Ripristina Luminosit\u00e0 Luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", + "title": "Opzioni ISY994" + } + } + }, + "title": "Universal Devices ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ko.json b/homeassistant/components/isy994/translations/ko.json new file mode 100644 index 00000000000..c0edb400594 --- /dev/null +++ b/homeassistant/components/isy994/translations/ko.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8 \ud56d\ubaa9\uc774 \uc644\uc804\ud55c URL \ud615\uc2dd\uc774 \uc544\ub2d9\ub2c8\ub2e4. \uc608: http://192.168.10.100:80", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "URL \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638", + "tls": "ISY \ucee8\ud2b8\ub864\ub7ec\uc758 TLS \ubc84\uc804.", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\ud638\uc2a4\ud2b8 \ud56d\ubaa9\uc740 \uc644\uc804\ud55c URL \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: http://192.168.10.100:80", + "title": "ISY994 \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "\ubb38\uc790\uc5f4 \ubb34\uc2dc", + "restore_light_state": "\uc870\uba85 \ubc1d\uae30 \ubcf5\uc6d0", + "sensor_string": "\ub178\ub4dc \uc13c\uc11c \ubb38\uc790\uc5f4", + "variable_sensor_string": "\ubcc0\uc218 \uc13c\uc11c \ubb38\uc790\uc5f4" + }, + "description": "ISY \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694: \n \u2022 \ub178\ub4dc \uc13c\uc11c \ubb38\uc790\uc5f4: \uc774\ub984\uc5d0 '\ub178\ub4dc \uc13c\uc11c \ubb38\uc790\uc5f4' \uc774 \ud3ec\ud568\ub41c \ubaa8\ub4e0 \uae30\uae30 \ub610\ub294 \ud3f4\ub354\ub294 \uc13c\uc11c \ub610\ub294 \uc774\uc9c4 \uc13c\uc11c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4. \n \u2022 \ubb38\uc790\uc5f4 \ubb34\uc2dc: \uc774\ub984\uc5d0 '\ubb38\uc790\uc5f4 \ubb34\uc2dc' \uac00 \ud3ec\ud568\ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \ubb34\uc2dc\ub429\ub2c8\ub2e4.\n \u2022 \ubcc0\uc218 \uc13c\uc11c \ubb38\uc790\uc5f4: '\ubcc0\uc218 \uc13c\uc11c \ubb38\uc790\uc5f4' \uc774 \ud3ec\ud568\ub41c \ubaa8\ub4e0 \ubcc0\uc218\ub294 \uc13c\uc11c\ub85c \ucd94\uac00\ub429\ub2c8\ub2e4.\n \u2022 \uc870\uba85 \ubc1d\uae30 \ubcf5\uc6d0: \ud65c\uc131\ud654\ud558\uba74 \uc870\uba85\uc744 \ucf24 \ub54c \uae30\uae30\uc758 \ub0b4\uc7a5\ub41c On-Level \ub300\uc2e0 \uc774\uc804 \ubc1d\uae30\uac00 \ubcf5\uc6d0\ub429\ub2c8\ub2e4.", + "title": "ISY994 \uc635\uc158" + } + } + }, + "title": "ISY994 \ubc94\uc6a9 \uae30\uae30" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/lb.json b/homeassistant/components/isy994/translations/lb.json new file mode 100644 index 00000000000..4d7d4cc47d7 --- /dev/null +++ b/homeassistant/components/isy994/translations/lb.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifkatioun", + "invalid_host": "Host Entr\u00e9e muss am URL Format sinn, beispill, http://192.168.10.100:80", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "Passwuert", + "tls": "TLS Versioun vum ISY Kontroller.", + "username": "Benotzernumm" + }, + "description": "Host Entr\u00e9e muss am URL Format sinn, beispill, http://192.168.10.100:80", + "title": "Mat dengem ISY994 verbannen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Zeechefolleg ignor\u00e9ieren", + "restore_light_state": "Hellegkeet vun der Luucht hierstellen", + "sensor_string": "Node Sensor Zeechefolleg", + "variable_sensor_string": "Variable Sensor Zeechefolleg" + }, + "description": "Optioune fir ISY Integratioun defin\u00e9ieren:\n \u2022 Node Sensor Zeechefolleg: All Apparat oder Dossier welch 'Node Sensor Zeechefolleg' am Numm enth\u00e4lt g\u00ebtt als Sensor oder bin\u00e4re Sensor trait\u00e9iert.\n \u2022 Zeechefolleg inor\u00e9ieren: All Apparat mam 'Zeechefolleg ignor\u00e9ieren' am Numm g\u00ebtt ignor\u00e9iert.\n \u2022 Variable Sensor Zeechefolleg: All Variable mat 'Variable Sensor Zeechefolleg' am Numm g\u00ebtt als Sensor dob\u00e4igesat\n \u2022 Hellegkeet vun der Luucht hierstellen: Falls aktiv\u00e9iert g\u00ebtt d\u00e9i Hellegkeet vu virdrun hiergestallt bei uschalte vun der Luucht am platz vun der Standard Hellegkeet vun der Luucht.", + "title": "ISY994 Optiounen" + } + } + }, + "title": "Universal Devices ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json new file mode 100644 index 00000000000..9d9f5b59db9 --- /dev/null +++ b/homeassistant/components/isy994/translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "invalid_host": "Vertsoppf\u00f8ringen var ikke i fullstendig URL-format, for eksempel http://192.168.10.100:80", + "unknown": "[%key:common::config_flow::error::unknown%" + }, + "step": { + "user": { + "data": { + "host": "URL", + "tls": "TLS-versjonen av ISY-kontrolleren." + }, + "description": "Vertsoppf\u00f8ringen m\u00e5 v\u00e6re i fullstendig URL-format, for eksempel http://192.168.10.100:80", + "title": "Koble til ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignorer streng", + "restore_light_state": "Gjenopprett lysstyrke", + "sensor_string": "Node sensor streng", + "variable_sensor_string": "Variabel sensorstreng" + }, + "description": "Angi alternativene for ISY-integrering: \n \u2022 Nodesensorstreng: Alle enheter eller mapper som inneholder NodeSensor String i navnet, behandles som en sensor eller bin\u00e6r sensor. \n \u2022 Ignorer streng: Alle enheter med 'Ignorer streng' i navnet ignoreres. \n \u2022 Variabel sensorstreng: Alle variabler som inneholder \"Variabel sensorstreng\" vil bli lagt til som en sensor. \n \u2022 Gjenopprett lyslysstyrke: Hvis den er aktivert, gjenopprettes den forrige lysstyrken n\u00e5r du sl\u00e5r p\u00e5 et lys i stedet for enhetens innebygde p\u00e5-niv\u00e5.", + "title": "ISY994 Alternativer" + } + } + }, + "title": "Universelle enheter ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json new file mode 100644 index 00000000000..a934b3d9f6c --- /dev/null +++ b/homeassistant/components/isy994/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key_id:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json new file mode 100644 index 00000000000..8c19d972f40 --- /dev/null +++ b/homeassistant/components/isy994/translations/ru.json @@ -0,0 +1,40 @@ +{ + "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": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "URL-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "tls": "\u0412\u0435\u0440\u0441\u0438\u044f TLS \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c", + "restore_light_state": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c \u0441\u0432\u0435\u0442\u0430", + "sensor_string": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0443\u0437\u0435\u043b \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440", + "variable_sensor_string": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440" + }, + "description": "\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432:\n \u2022 \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0443\u0437\u0435\u043b \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u043b\u044e\u0431\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u043f\u0430\u043f\u043a\u0430, \u0432 \u0438\u043c\u0435\u043d\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430, \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u043b\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u043b\u044e\u0431\u0430\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443, \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c: \u043b\u044e\u0431\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0432 \u0438\u043c\u0435\u043d\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430, \u0431\u0443\u0434\u0435\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f.\n \u2022 \u0412\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c \u0441\u0432\u0435\u0442\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u044f\u0440\u043a\u043e\u0441\u0442\u0438, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0435 \u0434\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ISY994" + } + } + }, + "title": "Universal Devices ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json new file mode 100644 index 00000000000..fa3ef3ebd19 --- /dev/null +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_host": "\u4e3b\u6a5f\u7aef\u4e26\u672a\u4ee5\u5b8c\u6574\u7db2\u5740\u683c\u5f0f\u8f38\u5165\uff0c\u4f8b\u5982\uff1ahttp://192.168.10.100:80", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u7db2\u5740", + "password": "\u5bc6\u78bc", + "tls": "ISY \u63a7\u5236\u5668 TLS \u7248\u672c", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u4e3b\u6a5f\u7aef\u5fc5\u9808\u4ee5\u5b8c\u6574\u7db2\u5740\u683c\u5f0f\u8f38\u5165\uff0c\u4f8b\u5982\uff1ahttp://192.168.10.100:80", + "title": "\u9023\u7dda\u81f3 ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "\u5ffd\u7565\u5b57\u4e32", + "restore_light_state": "\u56de\u5fa9\u4eae\u5ea6", + "sensor_string": "\u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32", + "variable_sensor_string": "\u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32" + }, + "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u8a2d\u5099\u90fd\u6703\u88ab\u8996\u70ba\u50b3\u611f\u5668\u6216\u4e8c\u9032\u4f4d\u50b3\u611f\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u8a2d\u5099\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u50b3\u611f\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u8a2d\u5099\u9810\u8a2d\u4eae\u5ea6\u3002", + "title": "ISY994 \u9078\u9805" + } + } + }, + "title": "Universal Devices ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 5390111890c..2a1a2eac0ca 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class ITachIP2IRRemote(remote.RemoteDevice): +class ITachIP2IRRemote(remote.RemoteEntity): """Device that sends commands to an ITachIP2IR device.""" def __init__(self, itachip2ir, name): diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index e96c40b13b6..707cac6f953 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -205,7 +205,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ItunesDevice(MediaPlayerDevice): +class ItunesDevice(MediaPlayerEntity): """Representation of an iTunes API instance.""" def __init__(self, name, host, port, use_ssl, add_entities): @@ -408,7 +408,7 @@ class ItunesDevice(MediaPlayerDevice): self.update_state(response) -class AirPlayDevice(MediaPlayerDevice): +class AirPlayDevice(MediaPlayerEntity): """Representation an AirPlay device via an iTunes API instance.""" def __init__(self, device_id, client): diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index b17313925a8..69c005b345b 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -4,7 +4,7 @@ from typing import List, Optional from pizone import Controller, Zone -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, FAN_HIGH, @@ -100,7 +100,7 @@ def _return_on_connection_error(ret=None): return wrap -class ControllerDevice(ClimateDevice): +class ControllerDevice(ClimateEntity): """Representation of iZone Controller.""" def __init__(self, controller: Controller) -> None: @@ -399,7 +399,7 @@ class ControllerDevice(ClimateDevice): await self.wrap_and_catch(self._controller.set_on(True)) -class ZoneDevice(ClimateDevice): +class ZoneDevice(ClimateEntity): """Representation of iZone Zone.""" def __init__(self, controller: ControllerDevice, zone: Zone) -> None: diff --git a/homeassistant/components/izone/translations/es-419.json b/homeassistant/components/izone/translations/es-419.json new file mode 100644 index 00000000000..b645c427e3e --- /dev/null +++ b/homeassistant/components/izone/translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos iZone en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n de iZone." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar iZone?", + "title": "iZone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/no.json b/homeassistant/components/izone/translations/no.json index 49c806aa564..854805948ee 100644 --- a/homeassistant/components/izone/translations/no.json +++ b/homeassistant/components/izone/translations/no.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere iZone?", + "description": "Vil du \u00e5 sette opp iZone?", "title": "" } } diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 7362fce3cd0..22e6a46e0ec 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -3,7 +3,7 @@ import logging import hdate -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity import homeassistant.util.dt as dt_util from . import DOMAIN, SENSOR_TYPES @@ -24,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class JewishCalendarBinarySensor(BinarySensorDevice): +class JewishCalendarBinarySensor(BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" def __init__(self, data, sensor, sensor_info): diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 969e193bac8..d333b9f913b 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,68 +1,115 @@ -"""Support for Juicenet cloud.""" +"""The JuiceNet integration.""" +import asyncio +from datetime import timedelta import logging -import pyjuicenet +import aiohttp +from pyjuicenet import Api, TokenError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .device import JuiceNetApi _LOGGER = logging.getLogger(__name__) -DOMAIN = "juicenet" +PLATFORMS = ["sensor", "switch"] CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, extra=vol.ALLOW_EXTRA, ) -JUICENET_COMPONENTS = ["sensor", "switch"] + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the JuiceNet component.""" + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True -def setup(hass, config): - """Set up the Juicenet component.""" - hass.data[DOMAIN] = {} +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up JuiceNet from a config entry.""" - access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) - hass.data[DOMAIN]["api"] = pyjuicenet.Api(access_token) + config = entry.data - for component in JUICENET_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + session = async_get_clientsession(hass) + + access_token = config[CONF_ACCESS_TOKEN] + api = Api(access_token, session) + + juicenet = JuiceNetApi(api) + + try: + await juicenet.setup() + except TokenError as error: + _LOGGER.error("JuiceNet Error %s", error) + return False + except aiohttp.ClientError as error: + _LOGGER.error("Could not reach the JuiceNet API %s", error) + raise ConfigEntryNotReady + + if not juicenet.devices: + _LOGGER.error("No JuiceNet devices found for this account") + return False + _LOGGER.info("%d JuiceNet device(s) found", len(juicenet.devices)) + + async def async_update_data(): + """Update all device states from the JuiceNet API.""" + for device in juicenet.devices: + await device.update_state(True) + return True + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="JuiceNet", + update_method=async_update_data, + update_interval=timedelta(seconds=30), + ) + + hass.data[DOMAIN][entry.entry_id] = { + JUICENET_API: juicenet, + JUICENET_COORDINATOR: coordinator, + } + + await coordinator.async_refresh() + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) return True -class JuicenetDevice(Entity): - """Represent a base Juicenet device.""" +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - def __init__(self, device, sensor_type, hass): - """Initialise the sensor.""" - self.hass = hass - self.device = device - self.type = sensor_type - - @property - def name(self): - """Return the name of the device.""" - return self.device.name() - - def update(self): - """Update state of the device.""" - self.device.update_state() - - @property - def _manufacturer_device_id(self): - """Return the manufacturer device id.""" - return self.device.id() - - @property - def _token(self): - """Return the device API token.""" - return self.device.token() - - @property - def unique_id(self): - """Return a unique ID.""" - return f"{self.device.id()}-{self.type}" + return unload_ok diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py new file mode 100644 index 00000000000..3f089300025 --- /dev/null +++ b/homeassistant/components/juicenet/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for JuiceNet integration.""" +import logging + +import aiohttp +from pyjuicenet import Api, TokenError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + juicenet = Api(data[CONF_ACCESS_TOKEN], session) + + try: + await juicenet.get_devices() + except TokenError as error: + _LOGGER.error("Token Error %s", error) + raise InvalidAuth + except aiohttp.ClientError as error: + _LOGGER.error("Error connecting %s", error) + raise CannotConnect + + # Return info that you want to store in the config entry. + return {"title": "JuiceNet"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for JuiceNet.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + + await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN]) + self._abort_if_unique_id_configured() + + try: + info = await validate_input(self.hass, user_input) + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/juicenet/const.py b/homeassistant/components/juicenet/const.py new file mode 100644 index 00000000000..5dc3e5c3e27 --- /dev/null +++ b/homeassistant/components/juicenet/const.py @@ -0,0 +1,6 @@ +"""Constants used by the JuiceNet component.""" + +DOMAIN = "juicenet" + +JUICENET_API = "juicenet_api" +JUICENET_COORDINATOR = "juicenet_coordinator" diff --git a/homeassistant/components/juicenet/device.py b/homeassistant/components/juicenet/device.py new file mode 100644 index 00000000000..37d36a2b24c --- /dev/null +++ b/homeassistant/components/juicenet/device.py @@ -0,0 +1,23 @@ +"""Adapter to wrap the pyjuicenet api for home assistant.""" + +import logging + +_LOGGER = logging.getLogger(__name__) + + +class JuiceNetApi: + """Represent a connection to JuiceNet.""" + + def __init__(self, api): + """Create an object from the provided API instance.""" + self.api = api + self._devices = [] + + async def setup(self): + """JuiceNet device setup.""" # noqa: D403 + self._devices = await self.api.get_devices() + + @property + def devices(self) -> list: + """Get a list of devices managed by this account.""" + return self._devices diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py new file mode 100644 index 00000000000..fe81f242cd0 --- /dev/null +++ b/homeassistant/components/juicenet/entity.py @@ -0,0 +1,54 @@ +"""Adapter to wrap the pyjuicenet api for home assistant.""" + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + + +class JuiceNetDevice(Entity): + """Represent a base JuiceNet device.""" + + def __init__(self, device, sensor_type, coordinator): + """Initialise the sensor.""" + self.device = device + self.type = sensor_type + self.coordinator = coordinator + + @property + def name(self): + """Return the name of the device.""" + return self.device.name + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self.coordinator.last_update_success + + async def async_update(self): + """Update the entity.""" + await self.coordinator.async_request_refresh() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self.device.id}-{self.type}" + + @property + def device_info(self): + """Return device information about this JuiceNet Device.""" + return { + "identifiers": {(DOMAIN, self.device.id)}, + "name": self.device.name, + "manufacturer": "JuiceNet", + } diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 79ba6ba9ec5..66b7912028e 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -2,6 +2,7 @@ "domain": "juicenet", "name": "JuiceNet", "documentation": "https://www.home-assistant.io/integrations/juicenet", - "requirements": ["python-juicenet==0.1.6"], - "codeowners": ["@jesserockz"] + "requirements": ["python-juicenet==1.0.1"], + "codeowners": ["@jesserockz"], + "config_flow": true } diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 63eeb6fc58f..e7408cf9f85 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -10,7 +10,8 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -from . import DOMAIN, JuicenetDevice +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice _LOGGER = logging.getLogger(__name__) @@ -25,38 +26,39 @@ SENSOR_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Juicenet sensor.""" - api = hass.data[DOMAIN]["api"] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the JuiceNet Sensors.""" + entities = [] + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] - dev = [] - for device in api.get_devices(): - for variable in SENSOR_TYPES: - dev.append(JuicenetSensorDevice(device, variable, hass)) - - add_entities(dev) + for device in api.devices: + for sensor in SENSOR_TYPES: + entities.append(JuiceNetSensorDevice(device, sensor, coordinator)) + async_add_entities(entities) -class JuicenetSensorDevice(JuicenetDevice, Entity): - """Implementation of a Juicenet sensor.""" +class JuiceNetSensorDevice(JuiceNetDevice, Entity): + """Implementation of a JuiceNet sensor.""" - def __init__(self, device, sensor_type, hass): + def __init__(self, device, sensor_type, coordinator): """Initialise the sensor.""" - super().__init__(device, sensor_type, hass) + super().__init__(device, sensor_type, coordinator) self._name = SENSOR_TYPES[sensor_type][0] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @property def name(self): """Return the name of the device.""" - return f"{self.device.name()} {self._name}" + return f"{self.device.name} {self._name}" @property def icon(self): """Return the icon of the sensor.""" icon = None if self.type == "status": - status = self.device.getStatus() + status = self.device.status if status == "standby": icon = "mdi:power-plug-off" elif status == "plugged": @@ -87,29 +89,19 @@ class JuicenetSensorDevice(JuicenetDevice, Entity): """Return the state.""" state = None if self.type == "status": - state = self.device.getStatus() + state = self.device.status elif self.type == "temperature": - state = self.device.getTemperature() + state = self.device.temperature elif self.type == "voltage": - state = self.device.getVoltage() + state = self.device.voltage elif self.type == "amps": - state = self.device.getAmps() + state = self.device.amps elif self.type == "watts": - state = self.device.getWatts() + state = self.device.watts elif self.type == "charge_time": - state = self.device.getChargeTime() + state = self.device.charge_time elif self.type == "energy_added": - state = self.device.getEnergyAdded() + state = self.device.energy_added else: state = "Unknown" return state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attributes = {} - if self.type == "status": - man_dev_id = self.device.id() - if man_dev_id: - attributes["manufacturer_device_id"] = man_dev_id - return attributes diff --git a/homeassistant/components/juicenet/strings.json b/homeassistant/components/juicenet/strings.json new file mode 100644 index 00000000000..4c8ffb8c62f --- /dev/null +++ b/homeassistant/components/juicenet/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "This JuiceNet account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API Token" + }, + "description": "You will need the API Token from https://home.juice.net/Manage.", + "title": "Connect to JuiceNet" + } + } + } +} diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py index 30bb5b22814..f09ec5559eb 100644 --- a/homeassistant/components/juicenet/switch.py +++ b/homeassistant/components/juicenet/switch.py @@ -1,45 +1,47 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity -from . import DOMAIN, JuicenetDevice +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Juicenet switch.""" - api = hass.data[DOMAIN]["api"] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the JuiceNet switches.""" + entities = [] + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] - devs = [] - for device in api.get_devices(): - devs.append(JuicenetChargeNowSwitch(device, hass)) - - add_entities(devs) + for device in api.devices: + entities.append(JuiceNetChargeNowSwitch(device, coordinator)) + async_add_entities(entities) -class JuicenetChargeNowSwitch(JuicenetDevice, SwitchDevice): - """Implementation of a Juicenet switch.""" +class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): + """Implementation of a JuiceNet switch.""" - def __init__(self, device, hass): + def __init__(self, device, coordinator): """Initialise the switch.""" - super().__init__(device, "charge_now", hass) + super().__init__(device, "charge_now", coordinator) @property def name(self): """Return the name of the device.""" - return f"{self.device.name()} Charge Now" + return f"{self.device.name} Charge Now" @property def is_on(self): """Return true if switch is on.""" - return self.device.getOverrideTime() != 0 + return self.device.override_time != 0 - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Charge now.""" - self.device.setOverride(True) + await self.device.set_override(True) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Don't charge now.""" - self.device.setOverride(False) + await self.device.set_override(False) diff --git a/homeassistant/components/juicenet/translations/ca.json b/homeassistant/components/juicenet/translations/ca.json new file mode 100644 index 00000000000..1d7cad2111b --- /dev/null +++ b/homeassistant/components/juicenet/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest compte de JuiceNet ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_token": "Token de l'API de JuiceNet" + }, + "description": "Necessitar\u00e0s la clau API de https://home.juice.net/Manage.", + "title": "Connexi\u00f3 amb JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/de.json b/homeassistant/components/juicenet/translations/de.json new file mode 100644 index 00000000000..16f48ef3837 --- /dev/null +++ b/homeassistant/components/juicenet/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses JuiceNet-Konto ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API Token" + }, + "description": "Sie ben\u00f6tigen das API-Token von https://home.juice.net/Manage.", + "title": "Stellen Sie eine Verbindung zu JuiceNet her" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/en.json b/homeassistant/components/juicenet/translations/en.json new file mode 100644 index 00000000000..faf21a8d617 --- /dev/null +++ b/homeassistant/components/juicenet/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "This JuiceNet account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API Token" + }, + "description": "You will need the API Token from https://home.juice.net/Manage.", + "title": "Connect to JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/es.json b/homeassistant/components/juicenet/translations/es.json new file mode 100644 index 00000000000..b8e3d3444c2 --- /dev/null +++ b/homeassistant/components/juicenet/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cuenta de JuiceNet ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_token": "Token API de JuiceNet" + }, + "description": "Necesitar\u00e1s el Token API de https://home.juice.net/Manage.", + "title": "Conectar a JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/fi.json b/homeassistant/components/juicenet/translations/fi.json new file mode 100644 index 00000000000..4d0640fc077 --- /dev/null +++ b/homeassistant/components/juicenet/translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "JuiceNet API-tunnus" + }, + "description": "Tarvitset API-tunnuksen osoitteesta https://home.juice.net/Manage.", + "title": "Yhdist\u00e4 JuiceNetiin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/fr.json b/homeassistant/components/juicenet/translations/fr.json new file mode 100644 index 00000000000..2448ec6263c --- /dev/null +++ b/homeassistant/components/juicenet/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ce compte JuiceNet est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_token": "Jeton d'API JuiceNet" + }, + "description": "Vous aurez besoin du jeton API de https://home.juice.net/Manage.", + "title": "Se connecter \u00e0 JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/he.json b/homeassistant/components/juicenet/translations/he.json new file mode 100644 index 00000000000..863e2560cbd --- /dev/null +++ b/homeassistant/components/juicenet/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + }, + "step": { + "user": { + "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05beAPI \u05de\u05behttps://home.juice.net/Manage." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/it.json b/homeassistant/components/juicenet/translations/it.json new file mode 100644 index 00000000000..be8eee9745d --- /dev/null +++ b/homeassistant/components/juicenet/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Questo account JuiceNet \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_token": "Token API JuiceNet" + }, + "description": "Avrete bisogno del Token API da https://home.juice.net/Manage.", + "title": "Connettersi a JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/ko.json b/homeassistant/components/juicenet/translations/ko.json new file mode 100644 index 00000000000..50b824ec82f --- /dev/null +++ b/homeassistant/components/juicenet/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 JuiceNet \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API \ud1a0\ud070" + }, + "description": "https://home.juice.net/Manage \uc758 API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.", + "title": "JuiceNet \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/lb.json b/homeassistant/components/juicenet/translations/lb.json new file mode 100644 index 00000000000..d4b1fb22bf8 --- /dev/null +++ b/homeassistant/components/juicenet/translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse JuiceNet Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API Jeton" + }, + "description": "Du brauchs een API Schl\u00ebssel vun https://home.juice.net/Manage.", + "title": "Mat JuiceNet verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/no.json b/homeassistant/components/juicenet/translations/no.json new file mode 100644 index 00000000000..1d0e3a15f5b --- /dev/null +++ b/homeassistant/components/juicenet/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Denne JuiceNet-kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API-token" + }, + "description": "Du trenger API-tokenet fra https://home.juice.net/Manage.", + "title": "Koble til JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/pl.json b/homeassistant/components/juicenet/translations/pl.json new file mode 100644 index 00000000000..4da73cd5cbf --- /dev/null +++ b/homeassistant/components/juicenet/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "api_token": "Token API JuiceNet" + }, + "description": "B\u0119dziesz potrzebowa\u0142 klucza API ze strony https://home.juice.net/Manage.", + "title": "Po\u0142\u0105czenie z JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/ru.json b/homeassistant/components/juicenet/translations/ru.json new file mode 100644 index 00000000000..3bb4084bac3 --- /dev/null +++ b/homeassistant/components/juicenet/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0443\u0436\u0435\u043d \u0442\u043e\u043a\u0435\u043d API \u0441 \u0441\u0430\u0439\u0442\u0430 https://home.juice.net/Manage.", + "title": "JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/sv.json b/homeassistant/components/juicenet/translations/sv.json new file mode 100644 index 00000000000..2872982e7cb --- /dev/null +++ b/homeassistant/components/juicenet/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Detta JuiceNet-konto \u00e4r redan konfigurerat" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API-Nyckel" + }, + "description": "Du beh\u00f6ver en API-Nyckel fr\u00e5n https://home.juice.net/Manage.", + "title": "Anslut till JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/zh-Hant.json b/homeassistant/components/juicenet/translations/zh-Hant.json new file mode 100644 index 00000000000..b54bb3d4676 --- /dev/null +++ b/homeassistant/components/juicenet/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "JuiceNet \u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_token": "JuiceNet API \u5bc6\u9470" + }, + "description": "\u5c07\u9700\u8981\u7531 https://home.juice.net/Manage \u53d6\u5f97 API \u5bc6\u9470\u3002", + "title": "\u9023\u7dda\u81f3 JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index d9f4db62572..ce179515e88 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -4,7 +4,7 @@ import logging import requests 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_HOST, CONF_NAME, @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): add_entities_callback(devices) -class KankunSwitch(SwitchDevice): +class KankunSwitch(SwitchEntity): """Representation of a Kankun Wifi switch.""" def __init__(self, hass, name, host, port, path, user, passwd): diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 5cced416bc3..5c9edfa7793 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PLUG, DEVICE_CLASS_POWER, DEVICE_CLASS_SAFETY, - BinarySensorDevice, + BinarySensorEntity, ) from . import DOMAIN @@ -36,7 +36,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class KebaBinarySensor(BinarySensorDevice): +class KebaBinarySensor(BinarySensorEntity): """Representation of a binary sensor of a KEBA charging station.""" def __init__(self, keba, key, name, entity_type, device_class): diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index f69fbdddf20..385adf662be 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -1,7 +1,7 @@ """Support for KEBA charging station switch.""" import logging -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from . import DOMAIN @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class KebaLock(LockDevice): +class KebaLock(LockEntity): """The entity class for KEBA charging stations switch.""" def __init__(self, keba, name, entity_type): diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index d888b30a3b9..cf87a7dd447 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -23,7 +23,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MediaPlayerDevice, + MediaPlayerEntity, ) from homeassistant.const import ( CONF_HOST, @@ -174,7 +174,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= add_service(SERVICE_SUB_DB, "sub_db", "db_value") -class KefMediaPlayer(MediaPlayerDevice): +class KefMediaPlayer(MediaPlayerEntity): """Kef Player Object.""" def __init__( diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 4a58f8f43c2..79971d5aa93 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -4,7 +4,7 @@ import logging from kiwiki import KiwiClient, KiwiException import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice +from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity from homeassistant.const import ( ATTR_ID, ATTR_LATITUDE, @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([KiwiLock(lock, kiwi) for lock in available_locks], True) -class KiwiLock(LockDevice): +class KiwiLock(LockEntity): """Representation of a Kiwi lock.""" def __init__(self, kiwi_lock, client): diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c302188ff20..bc1aa6c1301 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -4,15 +4,20 @@ import logging import voluptuous as vol from xknx import XKNX from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor +from xknx.dpt import DPTArray, DPTBinary from xknx.exceptions import XKNXException from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType -from xknx.knx import AddressFilter, DPTArray, DPTBinary, GroupAddress, Telegram +from xknx.telegram import AddressFilter, GroupAddress, Telegram from homeassistant.const import ( CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.helpers import discovery @@ -35,6 +40,8 @@ CONF_KNX_STATE_UPDATER = "state_updater" CONF_KNX_RATE_LIMIT = "rate_limit" CONF_KNX_EXPOSE = "expose" CONF_KNX_EXPOSE_TYPE = "type" +CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" +CONF_KNX_EXPOSE_DEFAULT = "default" CONF_KNX_EXPOSE_ADDRESS = "address" SERVICE_KNX_SEND = "send" @@ -57,6 +64,8 @@ EXPOSE_SCHEMA = vol.Schema( { vol.Required(CONF_KNX_EXPOSE_TYPE): cv.string, vol.Optional(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, + vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, } ) @@ -244,6 +253,8 @@ class KNXModule: for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]: expose_type = to_expose.get(CONF_KNX_EXPOSE_TYPE) entity_id = to_expose.get(CONF_ENTITY_ID) + attribute = to_expose.get(CONF_KNX_EXPOSE_ATTRIBUTE) + default = to_expose.get(CONF_KNX_EXPOSE_DEFAULT) address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS) if expose_type in ["time", "date", "datetime"]: exposure = KNXExposeTime(self.xknx, expose_type, address) @@ -251,7 +262,13 @@ class KNXModule: self.exposures.append(exposure) else: exposure = KNXExposeSensor( - self.hass, self.xknx, expose_type, entity_id, address + self.hass, + self.xknx, + expose_type, + entity_id, + attribute, + default, + address, ) exposure.async_register() self.exposures.append(exposure) @@ -325,23 +342,26 @@ class KNXExposeTime: class KNXExposeSensor: """Object to Expose Home Assistant entity to KNX bus.""" - def __init__(self, hass, xknx, expose_type, entity_id, address): + def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address): """Initialize of Expose class.""" self.hass = hass self.xknx = xknx self.type = expose_type self.entity_id = entity_id + self.expose_attribute = attribute + self.expose_default = default self.address = address self.device = None @callback def async_register(self): """Register listener.""" + if self.expose_attribute is not None: + _name = self.entity_id + "__" + self.expose_attribute + else: + _name = self.entity_id self.device = ExposeSensor( - self.xknx, - name=self.entity_id, - group_address=self.address, - value_type=self.type, + self.xknx, name=_name, group_address=self.address, value_type=self.type, ) self.xknx.devices.add(self.device) async_track_state_change(self.hass, self.entity_id, self._async_entity_changed) @@ -350,13 +370,31 @@ class KNXExposeSensor: """Handle entity change.""" if new_state is None: return - if new_state.state == "unknown": + if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): return - if self.type == "binary": - if new_state.state == "on": - await self.device.set(True) - elif new_state.state == "off": - await self.device.set(False) + if self.expose_attribute is not None: + new_attribute = new_state.attributes.get(self.expose_attribute) + if old_state is not None: + old_attribute = old_state.attributes.get(self.expose_attribute) + if old_attribute == new_attribute: + # don't send same value sequentially + return + await self._async_set_knx_value(new_attribute) else: - await self.device.set(new_state.state) + await self._async_set_knx_value(new_state.state) + + async def _async_set_knx_value(self, value): + """Set new value on xknx ExposeSensor.""" + if value is None: + if self.expose_default is None: + return + value = self.expose_default + + if self.type == "binary": + if value == STATE_ON: + value = True + elif value == STATE_OFF: + value = False + + await self.device.set(value) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 95e7e2cc400..29effaa7ebf 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -2,7 +2,7 @@ import voluptuous as vol from xknx.devices import BinarySensor -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -102,7 +102,7 @@ def async_add_entities_config(hass, config, async_add_entities): async_add_entities([entity]) -class KNXBinarySensor(BinarySensorDevice): +class KNXBinarySensor(BinarySensorEntity): """Representation of a KNX binary sensor.""" def __init__(self, device): @@ -124,6 +124,10 @@ class KNXBinarySensor(BinarySensorDevice): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index ab590540543..a58e5312c11 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -3,9 +3,9 @@ from typing import List, Optional import voluptuous as vol from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode -from xknx.knx import HVACOperationMode +from xknx.dpt import HVACOperationMode -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -192,7 +192,7 @@ def async_add_entities_config(hass, config, async_add_entities): async_add_entities([KNXClimate(climate)]) -class KNXClimate(ClimateDevice): +class KNXClimate(ClimateEntity): """Representation of a KNX climate device.""" def __init__(self, device): @@ -215,6 +215,11 @@ class KNXClimate(ClimateDevice): self.device.register_device_updated_cb(after_update_callback) self.device.mode.register_device_updated_cb(after_update_callback) + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + await self.device.mode.sync() + @property def name(self) -> str: """Return the name of the KNX device.""" @@ -279,7 +284,8 @@ class KNXClimate(ClimateDevice): return OPERATION_MODES.get( self.device.mode.operation_mode.value, HVAC_MODE_HEAT ) - return None + # default to "heat" + return HVAC_MODE_HEAT @property def hvac_modes(self) -> Optional[List[str]]: @@ -293,7 +299,9 @@ class KNXClimate(ClimateDevice): _operations.append(HVAC_MODE_HEAT) _operations.append(HVAC_MODE_OFF) - return [op for op in _operations if op is not None] + _modes = list(filter(None, _operations)) + # default to ["heat"] + return _modes if _modes else [HVAC_MODE_HEAT] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set operation mode.""" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 5c4aa762b5c..731105f6629 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -11,7 +11,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) from homeassistant.const import CONF_NAME from homeassistant.core import callback @@ -94,7 +94,7 @@ def async_add_entities_config(hass, config, async_add_entities): async_add_entities([KNXCover(cover)]) -class KNXCover(CoverDevice): +class KNXCover(CoverEntity): """Representation of a KNX cover.""" def __init__(self, device): @@ -116,6 +116,10 @@ class KNXCover(CoverDevice): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 8570c2eb09a..7ea5dc52155 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback @@ -132,7 +132,7 @@ def async_add_entities_config(hass, config, async_add_entities): async_add_entities([KNXLight(light)]) -class KNXLight(Light): +class KNXLight(LightEntity): """Representation of a KNX light.""" def __init__(self, device): @@ -162,6 +162,10 @@ class KNXLight(Light): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index ab26c4b6287..941a62d2d14 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,6 +2,6 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.11.2"], + "requirements": ["xknx==0.11.3"], "codeowners": ["@Julius2342"] } diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 2679170b03d..2d278ec04b4 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -77,6 +77,10 @@ class KNXSensor(Entity): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Update the state from KNX.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index ae798bf4c08..00b98f0224b 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -2,7 +2,7 @@ import voluptuous as vol from xknx.devices import Switch as XknxSwitch -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -52,7 +52,7 @@ def async_add_entities_config(hass, config, async_add_entities): async_add_entities([KNXSwitch(switch)]) -class KNXSwitch(SwitchDevice): +class KNXSwitch(SwitchEntity): """Representation of a KNX switch.""" def __init__(self, device): @@ -73,6 +73,10 @@ class KNXSwitch(SwitchDevice): """Store register state change callback.""" self.async_register_callbacks() + async def async_update(self): + """Request a state update from KNX bus.""" + await self.device.sync() + @property def name(self): """Return the name of the KNX device.""" diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 9470c8bb2c8..094bdf0984b 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -49,7 +49,7 @@ async def async_setup(hass, config): if any((CONF_PLATFORM, DOMAIN) in cfg.items() for cfg in config.get(MP_DOMAIN, [])): # Register the Kodi media_player services async def async_service_handler(service): - """Map services to methods on MediaPlayerDevice.""" + """Map services to methods on MediaPlayerEntity.""" method = SERVICE_TO_METHOD.get(service.service) if not method: return diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 33e7c014e40..ac31716b887 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.components.kodi import SERVICE_CALL_METHOD from homeassistant.components.kodi.const import DOMAIN -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -248,7 +248,7 @@ def cmd(func): return wrapper -class KodiDevice(MediaPlayerDevice): +class KodiDevice(MediaPlayerEntity): """Representation of a XBMC/Kodi device.""" def __init__( @@ -530,9 +530,10 @@ class KodiDevice(MediaPlayerDevice): If the media type cannot be detected, the player type is used. """ - if MEDIA_TYPES.get(self._item.get("type")) is None and self._players: + item_type = MEDIA_TYPES.get(self._item.get("type")) + if (item_type is None or item_type == "channel") and self._players: return MEDIA_TYPES.get(self._players[0]["type"]) - return MEDIA_TYPES.get(self._item.get("type")) + return item_type @property def media_duration(self): diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index 5cd270d5008..20494eee424 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -1,7 +1,7 @@ """Support for wired binary sensors attached to a Konnected device.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class KonnectedBinarySensor(BinarySensorDevice): +class KonnectedBinarySensor(BinarySensorEntity): """Representation of a Konnected binary sensor.""" def __init__(self, device_id, zone_num, data): diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index efb1e83a728..793a5ee3d21 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -23,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.network import get_url from .const import ( CONF_ACTIVATION, @@ -297,7 +298,7 @@ class AlarmPanel: # keeping self.hass.data check for backwards compatibility # newly configured integrations store this in the config entry desired_api_host = self.options.get(CONF_API_HOST) or ( - self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url + self.hass.data[DOMAIN].get(CONF_API_HOST) or get_url(self.hass) ) desired_api_endpoint = desired_api_host + ENDPOINT_ROOT diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index 4c87cb0bff6..4956c5bb112 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -8,8 +8,8 @@ "user": { "description": "Please enter the host information for your Konnected Panel.", "data": { - "host": "Konnected device IP address", - "port": "Konnected device port" + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" } }, "confirm": { @@ -97,7 +97,11 @@ } } }, - "error": { "bad_host": "Invalid Override API host url" }, - "abort": { "not_konn_panel": "Not a recognized Konnected.io device" } + "error": { + "bad_host": "Invalid Override API host url" + }, + "abort": { + "not_konn_panel": "Not a recognized Konnected.io device" + } } } diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index afe65f67f62..d35146410ed 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -11,7 +11,7 @@ }, "step": { "confirm": { - "description": "Model: {model} \nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d\u2019alarma Konnected.", + "description": "Model: {model} \nID: {id}\nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d'alarma Konnected.", "title": "Dispositiu Konnected llest" }, "import_confirm": { diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 36ab7150c1f..fd88b143914 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -89,7 +89,8 @@ "api_host": "API-Host-URL \u00fcberschreiben (optional)", "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" }, - "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel" + "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel", + "title": "Sonstiges konfigurieren" }, "options_switch": { "data": { @@ -100,7 +101,8 @@ "pause": "Pause zwischen Impulsen (ms) (optional)", "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)" }, - "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}" + "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}", + "title": "Konfigurieren Sie den schaltbaren Ausgang" } } } diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json index 11adeea4c9a..694255903c0 100644 --- a/homeassistant/components/konnected/translations/en.json +++ b/homeassistant/components/konnected/translations/en.json @@ -20,8 +20,8 @@ }, "user": { "data": { - "host": "Konnected device IP address", - "port": "Konnected device port" + "host": "IP address", + "port": "Port" }, "description": "Please enter the host information for your Konnected Panel.", "title": "Discover Konnected Device" diff --git a/homeassistant/components/konnected/translations/es-419.json b/homeassistant/components/konnected/translations/es-419.json new file mode 100644 index 00000000000..a63ba501960 --- /dev/null +++ b/homeassistant/components/konnected/translations/es-419.json @@ -0,0 +1,108 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", + "not_konn_panel": "No es un dispositivo Konnected.io reconocido", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "cannot_connect": "No se puede conectar a un Panel Konnected en {host}: {port}" + }, + "step": { + "confirm": { + "description": "Modelo: {model} \nID: {id} \nHost: {host} \nPuerto: {port} \n\nPuede configurar el comportamiento de IO y del panel en la configuraci\u00f3n del Panel de alarmas conectadas.", + "title": "Dispositivo Konnected listo" + }, + "import_confirm": { + "description": "Se ha descubierto un Panel de alarma conectado con ID {id} en configuration.yaml. Este flujo le permitir\u00e1 importarlo a una entrada de configuraci\u00f3n.", + "title": "Importar dispositivo Konnected" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP del dispositivo Konnected", + "port": "Puerto de dispositivo Konnected" + }, + "description": "Ingrese la informaci\u00f3n del host para su Panel Konnected.", + "title": "Descubrir el dispositivo Konnected" + } + } + }, + "options": { + "abort": { + "not_konn_panel": "No es un dispositivo Konnected.io reconocido" + }, + "error": { + "bad_host": "URL de host de API de sobrescritura no v\u00e1lida" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertir el estado abierto/cerrado", + "name": "Nombre (opcional)", + "type": "Tipo de sensor binario" + }, + "description": "Seleccione las opciones para el sensor binario conectado a {zone}", + "title": "Configurar sensor binario" + }, + "options_digital": { + "data": { + "name": "Nombre (opcional)", + "poll_interval": "Intervalo de sondeo (minutos) (opcional)", + "type": "Tipo de sensor" + }, + "description": "Seleccione las opciones para el sensor digital conectado a {zone}", + "title": "Configurar sensor digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "SALIDA" + }, + "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada E/S a continuaci\u00f3n: seg\u00fan la E/S, puede permitir sensores binarios (contactos de apertura/cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARMA1", + "alarm2_out2": "SALIDA2/ALARMA2", + "out1": "SALIDA1" + }, + "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S extendida" + }, + "options_misc": { + "data": { + "api_host": "Sobrescribir URL de host de API (opcional)", + "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", + "override_api_host": "Sobrescribir la URL predeterminada del panel de host de la API de Home Assistant" + }, + "description": "Seleccione el comportamiento deseado para su panel", + "title": "Configurar Misc" + }, + "options_switch": { + "data": { + "activation": "Salida cuando est\u00e1 encendido", + "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "more_states": "Configurar estados adicionales para esta zona", + "name": "Nombre (opcional)", + "pause": "Pausa entre pulsos (ms) (opcional)", + "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" + }, + "description": "Seleccione las opciones de salida para {zone}: state {state}", + "title": "Configurar salida conmutable" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index 566c623c80c..a463803a8fe 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -6,8 +6,12 @@ "not_konn_panel": "Non reconnu comme appareil Konnected.io", "unknown": "Une erreur inconnue s'est produite" }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 Konnected Panel sur {host} : {port}" + }, "step": { "confirm": { + "description": "Model: {model} \n ID: {id} \n Host: {host} \n Port: {port} \n\nVous pouvez configurer le comportement des E/S et du panneau dans les param\u00e8tres de Konnected Alarm Panel.", "title": "Appareil Konnected pr\u00eat" }, "import_confirm": { @@ -15,12 +19,18 @@ }, "user": { "data": { - "host": "Adresse IP de l\u2019appareil Konnected" - } + "host": "Adresse IP de l\u2019appareil Konnected", + "port": "Port de l'appareil Konnected" + }, + "description": "Veuillez saisir les informations de l\u2019h\u00f4te de votre panneau Konnected.", + "title": "D\u00e9couverte d\u2019appareil Konnected" } } }, "options": { + "abort": { + "not_konn_panel": "Non reconnu comme appareil Konnected.io" + }, "error": { "one": "Vide", "other": "Vide" @@ -52,8 +62,10 @@ "4": "Zone 4", "5": "Zone 5", "6": "Zone 6", - "7": "Zone 7" - } + "7": "Zone 7", + "out": "OUT" + }, + "title": "Configurer les E/S" }, "options_io_ext": { "data": { @@ -62,13 +74,23 @@ "12": "Zone 12", "8": "Zone 8", "9": "Zone 9", - "alarm1": "ALARME1" - } + "alarm1": "ALARME1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "title": "Configurer les E/S \u00e9tendues" + }, + "options_misc": { + "title": "Configurer divers" }, "options_switch": { "data": { + "activation": "Sortie lorsque activ\u00e9", + "momentary": "Dur\u00e9e de l'impulsion (ms) (facultatif)", "more_states": "Configurer des \u00e9tats suppl\u00e9mentaires pour cette zone", - "name": "Nom (facultatif)" + "name": "Nom (facultatif)", + "pause": "Pause entre les impulsions (ms) (facultatif)", + "repeat": "Nombre de r\u00e9p\u00e9tition (-1=infini) (facultatif)" } } } diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json index 6945c5ae47d..ba36f231141 100644 --- a/homeassistant/components/konnected/translations/ko.json +++ b/homeassistant/components/konnected/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, @@ -20,8 +20,8 @@ }, "user": { "data": { - "host": "Konnected \uae30\uae30 IP \uc8fc\uc18c", - "port": "Konnected \uae30\uae30 \ud3ec\ud2b8" + "host": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" }, "description": "Konnected \ud328\ub110\uc758 \ud638\uc2a4\ud2b8 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "Konnected \uae30\uae30 \ucc3e\uae30" @@ -43,7 +43,7 @@ "type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615" }, "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \uc774\uc9c4 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "\uc774\uc9c4 \uc13c\uc11c \uad6c\uc131" + "title": "\uc774\uc9c4 \uc13c\uc11c \uad6c\uc131\ud558\uae30" }, "options_digital": { "data": { @@ -52,7 +52,7 @@ "type": "\uc13c\uc11c \uc720\ud615" }, "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \ub514\uc9c0\ud138 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "\ub514\uc9c0\ud138 \uc13c\uc11c \uad6c\uc131" + "title": "\ub514\uc9c0\ud138 \uc13c\uc11c \uad6c\uc131\ud558\uae30" }, "options_io": { "data": { @@ -66,7 +66,7 @@ "out": "\uc678\ubd80" }, "description": "{host} \uc5d0\uc11c {model} \uc744(\ub97c) \ubc1c\uacac\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc9c4 \uc13c\uc11c(\uac1c\ud3d0 \uc811\uc810), \ub514\uc9c0\ud138 \uc13c\uc11c(dht \ubc0f ds18b20) \ub610\ub294 \uc2a4\uc704\uce58\uac00 \uac00\ub2a5\ud55c \uc13c\uc11c\uc758 I/O \uc5d0 \ub530\ub77c \uc544\ub798\uc5d0\uc11c \uac01 I/O \uc758 \uae30\ubcf8 \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "I/O \uad6c\uc131" + "title": "I/O \uad6c\uc131\ud558\uae30" }, "options_io_ext": { "data": { @@ -80,7 +80,7 @@ "out1": "\ucd9c\ub825 1" }, "description": "\uc544\ub798\uc758 \ub098\uba38\uc9c0 I/O \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "\ud655\uc7a5 I/O \uad6c\uc131" + "title": "\ud655\uc7a5 I/O \uad6c\uc131\ud558\uae30" }, "options_misc": { "data": { @@ -89,7 +89,7 @@ "override_api_host": "\uae30\ubcf8 Home Assistant API \ud638\uc2a4\ud2b8 \ud328\ub110 URL \uc7ac\uc815\uc758" }, "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "\uae30\ud0c0 \uad6c\uc131" + "title": "\uae30\ud0c0 \uc124\uc815 \uad6c\uc131\ud558\uae30" }, "options_switch": { "data": { @@ -101,7 +101,7 @@ "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" }, "description": "{zone} \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694: \uc0c1\ud0dc {state}", - "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131" + "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index c513a9a0d7b..17bb20be765 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -8,6 +8,10 @@ "confirm": { "title": "Konnected Apparaat Klaar" }, + "import_confirm": { + "description": "Er is een Konnected Alarmpaneel met ID {id} ontdekt in configuration.yaml. Met deze flow kunt u deze importeren in een configuratie-item.", + "title": "Konnected apparaat importeren" + }, "user": { "data": { "host": "IP-adres van Konnected apparaat", @@ -23,6 +27,7 @@ "not_konn_panel": "Geen herkend Konnected.io apparaat" }, "error": { + "bad_host": "Ongeldige URL voor overschrijven API-host", "one": "Leeg", "other": "Leeg" }, @@ -58,16 +63,23 @@ "11": "Zone 11", "12": "Zone 12", "8": "Zone 8", - "9": "Zone 9" + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2 / ALARM2", + "out1": "OUT1" } }, "options_misc": { "data": { - "api_host": "API host-URL overschrijven (optioneel)" - } + "api_host": "API host-URL overschrijven (optioneel)", + "override_api_host": "Overschrijf standaard Home Assistant API hostpaneel-URL" + }, + "description": "Selecteer het gewenste gedrag voor uw paneel", + "title": "Configureer Misc" }, "options_switch": { "data": { + "more_states": "Aanvullende statussen voor deze zone configureren", "name": "Naam (optioneel)" }, "title": "Schakelbare uitgang configureren" diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index fe1879875be..b0504c4e3aa 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -80,7 +80,7 @@ "out1": "OUT1" }, "description": "Velg konfigurasjonen av de gjenv\u00e6rende I/O nedenfor. Du vil v\u00e6re i stand til \u00e5 konfigurere detaljerte alternativer i de neste trinnene.", - "title": "Konfigurer utvidet I / O" + "title": "Konfigurer utvidet I/O" }, "options_misc": { "data": { @@ -89,7 +89,7 @@ "override_api_host": "Overstyre standard Home Assistant API-vertspanel-URL" }, "description": "Vennligst velg \u00f8nsket atferd for din panel", - "title": "Konfigurere Diverse" + "title": "Konfigurer diverse" }, "options_switch": { "data": { @@ -101,7 +101,7 @@ "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)" }, "description": "Velg outputalternativer for {zone} : state {state}", - "title": "Konfigurere Valgbare Utgang" + "title": "Konfigurer utskiftbar utgang" } } } diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index 860d90536da..a71d458bd32 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io", - "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}" @@ -21,7 +21,7 @@ "user": { "data": { "host": "Adres IP urz\u0105dzenia Konnected", - "port": "Port urz\u0105dzenia Konnected urz\u0105dzenia" + "port": "[%key_id:common::config_flow::data::port%] urz\u0105dzenia Konnected" }, "description": "Wprowad\u017a informacje o ho\u015bcie panelu Konnected.", "title": "Wykryj urz\u0105dzenie Konnected" @@ -33,6 +33,7 @@ "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io" }, "error": { + "bad_host": "Nieprawid\u0142owy adres URL hosta zast\u0119puj\u0105cego dla interfejsu API", "few": "kilka", "many": "wiele", "one": "jeden", @@ -68,7 +69,7 @@ "7": "Strefa 7", "out": "OUT" }, - "description": "Wykryto {model} na ho\u015bcie {host}. Wybierz podstawow\u0105 konfiguracj\u0119 ka\u017cdego wej\u015bcia/wyj\u015bcia poni\u017cej \u2014 w zale\u017cno\u015bci od typu wej\u015b\u0107/wyj\u015b\u0107 mo\u017ce zastosowa\u0107 sensory binarne (otwarte/ amkni\u0119te), sensory cyfrowe (dht i ds18b20) lub prze\u0142\u0105czane wyj\u015bcia. B\u0119dziesz m\u00f3g\u0142 skonfigurowa\u0107 szczeg\u00f3\u0142owe opcje w kolejnych krokach.", + "description": "Wykryto {model} na ho\u015bcie {host}. Wybierz podstawow\u0105 konfiguracj\u0119 ka\u017cdego wej\u015bcia/wyj\u015bcia poni\u017cej \u2014 w zale\u017cno\u015bci od typu wej\u015b\u0107/wyj\u015b\u0107 mo\u017ce zastosowa\u0107 sensory binarne (otwarte/zamkni\u0119te), sensory cyfrowe (dht i ds18b20) lub prze\u0142\u0105czane wyj\u015bcia. B\u0119dziesz m\u00f3g\u0142 skonfigurowa\u0107 szczeg\u00f3\u0142owe opcje w kolejnych krokach.", "title": "Konfiguracja wej\u015bcia/wyj\u015bcia" }, "options_io_ext": { @@ -87,7 +88,9 @@ }, "options_misc": { "data": { - "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu" + "api_host": "Zast\u0119powanie adresu URL hosta API (opcjonalnie)", + "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu", + "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistant'a" }, "description": "Wybierz po\u017c\u0105dane zachowanie dla swojego panelu", "title": "R\u00f3\u017cne opcje" diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json index b4f8d74d68f..75ee7761d55 100644 --- a/homeassistant/components/konnected/translations/ru.json +++ b/homeassistant/components/konnected/translations/ru.json @@ -20,7 +20,7 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "IP-\u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u043f\u0430\u043d\u0435\u043b\u0438 Konnected.", diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 5a1c8fe08bd..4660e3c2f29 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -20,8 +20,8 @@ }, "user": { "data": { - "host": "Konnected \u8a2d\u5099 IP \u4f4d\u5740", - "port": "Konnected \u8a2d\u5099\u901a\u8a0a\u57e0" + "host": "IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" }, "description": "\u8acb\u8f38\u5165 Konnected \u9762\u677f\u4e3b\u6a5f\u7aef\u8cc7\u8a0a\u3002", "title": "\u641c\u7d22 Konnected \u8a2d\u5099" diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 249adf04af8..7b4cedfebad 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -1,7 +1,7 @@ """Support for LCN binary sensors.""" import pypck -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_ADDRESS from . import LcnDevice @@ -36,7 +36,7 @@ async def async_setup_platform( async_add_entities(devices) -class LcnRegulatorLockSensor(LcnDevice, BinarySensorDevice): +class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): """Representation of a LCN binary sensor for regulator locks.""" def __init__(self, config, address_connection): @@ -71,7 +71,7 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorDevice): self.async_write_ha_state() -class LcnBinarySensor(LcnDevice, BinarySensorDevice): +class LcnBinarySensor(LcnDevice, BinarySensorEntity): """Representation of a LCN binary sensor for binary sensor ports.""" def __init__(self, config, address_connection): @@ -103,7 +103,7 @@ class LcnBinarySensor(LcnDevice, BinarySensorDevice): self.async_write_ha_state() -class LcnLockKeysSensor(LcnDevice, BinarySensorDevice): +class LcnLockKeysSensor(LcnDevice, BinarySensorEntity): """Representation of a LCN sensor for key locks.""" def __init__(self, config, address_connection): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 12fff2f479b..9634dcf8fb3 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -2,7 +2,7 @@ import pypck -from homeassistant.components.climate import ClimateDevice, const +from homeassistant.components.climate import ClimateEntity, const from homeassistant.const import ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT from . import LcnDevice @@ -38,7 +38,7 @@ async def async_setup_platform( async_add_entities(devices) -class LcnClimate(LcnDevice, ClimateDevice): +class LcnClimate(LcnDevice, ClimateEntity): """Representation of a LCN climate device.""" def __init__(self, config, address_connection): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 61ae05fa010..05ee17a7daf 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,7 +1,7 @@ """Support for LCN covers.""" import pypck -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverEntity from homeassistant.const import CONF_ADDRESS from . import LcnDevice @@ -32,7 +32,7 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputsCover(LcnDevice, CoverDevice): +class LcnOutputsCover(LcnDevice, CoverEntity): """Representation of a LCN cover connected to output ports.""" def __init__(self, config, address_connection): @@ -111,7 +111,7 @@ class LcnOutputsCover(LcnDevice, CoverDevice): self.async_write_ha_state() -class LcnRelayCover(LcnDevice, CoverDevice): +class LcnRelayCover(LcnDevice, CoverEntity): """Representation of a LCN cover connected to relays.""" def __init__(self, config, address_connection): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 7f1cd547c02..e76becc0e9f 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -6,7 +6,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_ADDRESS @@ -47,7 +47,7 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputLight(LcnDevice, Light): +class LcnOutputLight(LcnDevice, LightEntity): """Representation of a LCN light for output ports.""" def __init__(self, config, address_connection): @@ -135,7 +135,7 @@ class LcnOutputLight(LcnDevice, Light): self.async_write_ha_state() -class LcnRelayLight(LcnDevice, Light): +class LcnRelayLight(LcnDevice, LightEntity): """Representation of a LCN light for relay ports.""" def __init__(self, config, address_connection): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index a2adda95b3b..e441ce40383 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,7 +1,7 @@ """Support for LCN switches.""" import pypck -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_ADDRESS from . import LcnDevice @@ -34,7 +34,7 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputSwitch(LcnDevice, SwitchDevice): +class LcnOutputSwitch(LcnDevice, SwitchEntity): """Representation of a LCN switch for output ports.""" def __init__(self, config, address_connection): @@ -79,7 +79,7 @@ class LcnOutputSwitch(LcnDevice, SwitchDevice): self.async_write_ha_state() -class LcnRelaySwitch(LcnDevice, SwitchDevice): +class LcnRelaySwitch(LcnDevice, SwitchEntity): """Representation of a LCN switch for relay ports.""" def __init__(self, config, address_connection): diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index cb91257f83d..d4e5fc119ac 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -7,7 +7,7 @@ from requests import RequestException import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([LgTVDevice(client, name, on_action_script)], True) -class LgTVDevice(MediaPlayerDevice): +class LgTVDevice(MediaPlayerEntity): """Representation of a LG TV.""" def __init__(self, client, name, on_action_script): diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 30cfbf17074..f3c89d6138e 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -3,7 +3,7 @@ import logging import temescal -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([LGDevice(discovery_info)], True) -class LGDevice(MediaPlayerDevice): +class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" def __init__(self, discovery_info): diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index e0ea645b197..e24bb3639b6 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -47,8 +47,8 @@ class Life360ConfigFlow(config_entries.ConfigFlow): try: # pylint: disable=no-value-for-parameter vol.Email()(self._username) - authorization = self._api.get_authorization( - self._username, self._password + authorization = await self.hass.async_add_executor_job( + self._api.get_authorization, self._username, self._password ) except vol.Invalid: errors[CONF_USERNAME] = "invalid_username" @@ -89,7 +89,9 @@ class Life360ConfigFlow(config_entries.ConfigFlow): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] try: - authorization = self._api.get_authorization(username, password) + authorization = await self.hass.async_add_executor_job( + self._api.get_authorization, username, password + ) except LoginError: _LOGGER.error("Invalid credentials for %s", username) return self.async_abort(reason="invalid_credentials") diff --git a/homeassistant/components/life360/strings.json b/homeassistant/components/life360/strings.json index 22d3f52c63b..0e91856c8fc 100644 --- a/homeassistant/components/life360/strings.json +++ b/homeassistant/components/life360/strings.json @@ -3,14 +3,17 @@ "step": { "user": { "title": "Life360 Account Info", - "data": { "username": "Username", "password": "Password" }, + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts." } }, "error": { "invalid_username": "Invalid username", "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured", + "user_already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "unexpected": "Unexpected error communicating with Life360 server" }, "create_entry": { @@ -18,7 +21,7 @@ }, "abort": { "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured" + "user_already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/bg.json b/homeassistant/components/life360/translations/bg.json index 9c8dca0b06f..115639e5c3f 100644 --- a/homeassistant/components/life360/translations/bg.json +++ b/homeassistant/components/life360/translations/bg.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", - "user_already_configured": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0440\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438, \u0432\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043d\u0430 Life360]({docs_url})." diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index a61e4372b78..0c4b760df0b 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Credencials inv\u00e0lides", - "user_already_configured": "El compte ja ha estat configurat" + "user_already_configured": "[%key::common::config_flow::abort::already_configured_account%]" }, "create_entry": { "default": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Credencials inv\u00e0lides", "invalid_username": "Nom d'usuari incorrecte", "unexpected": "S'ha produ\u00eft un error inesperat en comunicar-se amb el servidor de Life360.", - "user_already_configured": "El compte ja ha estat configurat" + "user_already_configured": "[%key::common::config_flow::abort::already_configured_account%]" }, "step": { "user": { @@ -19,7 +19,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d\u2019afegir cap compte.", + "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d'afegir cap compte.", "title": "Informaci\u00f3 del compte Life360" } } diff --git a/homeassistant/components/life360/translations/da.json b/homeassistant/components/life360/translations/da.json index 76f46e7c816..7033496add3 100644 --- a/homeassistant/components/life360/translations/da.json +++ b/homeassistant/components/life360/translations/da.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_credentials": "Ugyldige legitimationsoplysninger", - "user_already_configured": "Kontoen er allerede konfigureret" + "invalid_credentials": "Ugyldige legitimationsoplysninger" }, "create_entry": { "default": "Hvis du vil angive avancerede indstillinger skal du se [Life360 dokumentation]({docs_url})." diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 09970c59014..d689492ad58 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", - "user_already_configured": "Konto wurde bereits konfiguriert" + "user_already_configured": "Account ist bereits konfiguriert" }, "create_entry": { "default": "M\u00f6gliche erweiterte Einstellungen finden sich unter [Life360-Dokumentation]({docs_url})." diff --git a/homeassistant/components/life360/translations/en.json b/homeassistant/components/life360/translations/en.json index 18f22d7f1b8..a305dacc8c3 100644 --- a/homeassistant/components/life360/translations/en.json +++ b/homeassistant/components/life360/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured" + "user_already_configured": "Account is already configured" }, "create_entry": { "default": "To set advanced options, see [Life360 documentation]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Invalid credentials", "invalid_username": "Invalid username", "unexpected": "Unexpected error communicating with Life360 server", - "user_already_configured": "Account has already been configured" + "user_already_configured": "Account is already configured" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/es-419.json b/homeassistant/components/life360/translations/es-419.json index 33d8d59d080..fcc0561f11f 100644 --- a/homeassistant/components/life360/translations/es-419.json +++ b/homeassistant/components/life360/translations/es-419.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "user_already_configured": "La cuenta ya ha sido configurada" + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Para establecer opciones avanzadas, consulte [Documentaci\u00f3n de Life360] ({docs_url})." }, "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", @@ -15,6 +18,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, + "description": "Para establecer opciones avanzadas, consulte [Documentaci\u00f3n de Life360] ( {docs_url} ). \n Es posible que desee hacer eso antes de agregar cuentas.", "title": "Informaci\u00f3n de la cuenta Life360" } } diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index fc6c58d8ed8..72bedd1efb9 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Credenciales no v\u00e1lidas", - "user_already_configured": "La cuenta ya ha sido configurada" + "user_already_configured": "La cuenta ya est\u00e1 configurada" }, "create_entry": { "default": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url})." diff --git a/homeassistant/components/life360/translations/fi.json b/homeassistant/components/life360/translations/fi.json new file mode 100644 index 00000000000..e40bae697f6 --- /dev/null +++ b/homeassistant/components/life360/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Life360-tilitiedot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index a08c6bc6efe..cf004d4c03a 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Informations d'identification invalides", - "user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "user_already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" }, "create_entry": { "default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )." diff --git a/homeassistant/components/life360/translations/hr.json b/homeassistant/components/life360/translations/hr.json index 5cf8cbef17f..00cced7f635 100644 --- a/homeassistant/components/life360/translations/hr.json +++ b/homeassistant/components/life360/translations/hr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_credentials": "Neva\u017ee\u0107e vjerodajnice", - "user_already_configured": "Ra\u010dun je ve\u0107 konfiguriran" + "invalid_credentials": "Neva\u017ee\u0107e vjerodajnice" }, "create_entry": { "default": "Da biste postavili napredne opcije, pogledajte [Life360 dokumentacija] ( {docs_url} )." diff --git a/homeassistant/components/life360/translations/it.json b/homeassistant/components/life360/translations/it.json index 3142208b70c..8239a0fbe83 100644 --- a/homeassistant/components/life360/translations/it.json +++ b/homeassistant/components/life360/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Credenziali non valide", - "user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato" + "user_already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "create_entry": { "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Credenziali non valide", "invalid_username": "Nome utente non valido", "unexpected": "Errore imprevisto durante la comunicazione con il server di Life360", - "user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato" + "user_already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index ad1ececc858..fac13b7b3da 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_credentials": "Ongeldige gebruikersgegevens", - "user_already_configured": "Account is al geconfigureerd" + "invalid_credentials": "Ongeldige gebruikersgegevens" }, "create_entry": { "default": "Om geavanceerde opties in te stellen, zie [Life360 documentatie]({docs_url})." diff --git a/homeassistant/components/life360/translations/no.json b/homeassistant/components/life360/translations/no.json index fba620789ab..1abca20c9dc 100644 --- a/homeassistant/components/life360/translations/no.json +++ b/homeassistant/components/life360/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Ugyldig legitimasjon", - "user_already_configured": "Kontoen er allerede konfigurert" + "user_already_configured": "Konto er allerede konfigurert" }, "create_entry": { "default": "For \u00e5 angi avanserte alternativer, se [Life360 dokumentasjon]({docs_url})." @@ -10,8 +10,7 @@ "error": { "invalid_credentials": "Ugyldig legitimasjon", "invalid_username": "Ugyldig brukernavn", - "unexpected": "Uventet feil under kommunikasjon med Life360-servern", - "user_already_configured": "Kontoen er allerede konfigurert" + "unexpected": "Uventet feil under kommunikasjon med Life360-servern" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 19a6c6d8828..1bf35e8853b 100644 --- a/homeassistant/components/life360/translations/pl.json +++ b/homeassistant/components/life360/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "user_already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." @@ -16,8 +16,8 @@ "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url}). Mo\u017cesz to zrobi\u0107 przed dodaniem kont.", "title": "Informacje o koncie Life360" diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index b841e7b28fd..9136fa3f115 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_credentials": "Credenciais inv\u00e1lidas", - "user_already_configured": "A conta j\u00e1 foi configurada" + "invalid_credentials": "Credenciais inv\u00e1lidas" }, "create_entry": { "default": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url})." diff --git a/homeassistant/components/life360/translations/sl.json b/homeassistant/components/life360/translations/sl.json index 857f12041d1..1e65cb692de 100644 --- a/homeassistant/components/life360/translations/sl.json +++ b/homeassistant/components/life360/translations/sl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Napa\u010dno geslo", - "user_already_configured": "Ra\u010dun \u017ee nastavljen" + "user_already_configured": "Ra\u010dun je \u017ee nastavljen" }, "create_entry": { "default": "\u010ce \u017eelite nastaviti napredne mo\u017enosti, glejte [Life360 dokumentacija]({docs_url})." diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 915c7e7f55b..b8815f53643 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_credentials": "Ogiltiga autentiseringsuppgifter", - "user_already_configured": "Konto har redan konfigurerats" + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index ca04dbefb7f..f36b64f2397 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -32,7 +32,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, - Light, + LightEntity, preprocess_turn_on_alternatives, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP @@ -438,7 +438,7 @@ def convert_16_to_8(value): return value >> 8 -class LIFXLight(Light): +class LIFXLight(LightEntity): """Representation of a LIFX light.""" def __init__(self, bulb, effects_conductor): diff --git a/homeassistant/components/lifx/translations/fi.json b/homeassistant/components/lifx/translations/fi.json new file mode 100644 index 00000000000..16fffbb2e5c --- /dev/null +++ b/homeassistant/components/lifx/translations/fi.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 LIFX:n?", + "title": "LIFX" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/no.json b/homeassistant/components/lifx/translations/no.json index 82f99c45e79..e646db4b2ad 100644 --- a/homeassistant/components/lifx/translations/no.json +++ b/homeassistant/components/lifx/translations/no.json @@ -7,7 +7,7 @@ "step": { "confirm": { "description": "\u00d8nsker du \u00e5 sette opp LIFX?", - "title": "LIFX" + "title": "" } } } diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py index 7fb0e686b31..f0ed9105b99 100644 --- a/homeassistant/components/lifx_legacy/light.py +++ b/homeassistant/components/lifx_legacy/light.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - Light, + LightEntity, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_change @@ -137,7 +137,7 @@ class LIFX: self._liffylights.probe(address) -class LIFXLight(Light): +class LIFXLight(LightEntity): """Representation of a LIFX light.""" def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness, kelvin): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7c33fdcb075..0a3c087950e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -167,12 +167,19 @@ def preprocess_turn_on_alternatives(params): if rgb_color is not None: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + return params + + +def filter_turn_off_params(params): + """Filter out params not used in turn off.""" + return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} + def preprocess_turn_off(params): """Process data for turning light off if brightness is 0.""" if ATTR_BRIGHTNESS in params and params[ATTR_BRIGHTNESS] == 0: # Zero brightness: Light will be turned off - params = {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} + params = filter_turn_off_params(params) return (True, params) # Light should be turned off return (False, None) # Light should be turned on @@ -198,13 +205,7 @@ async def async_setup(hass, config): if entity_field in data } - preprocess_turn_on_alternatives(data) - turn_lights_off, off_params = preprocess_turn_off(data) - - base["params"] = data - base["turn_lights_off"] = turn_lights_off - base["off_params"] = off_params - + base["params"] = preprocess_turn_on_alternatives(data) return base async def async_handle_light_on_service(light, call): @@ -213,8 +214,6 @@ async def async_setup(hass, config): If brightness is set to 0, this service will turn the light off. """ params = call.data["params"] - turn_light_off = call.data["turn_lights_off"] - off_params = call.data["off_params"] if not params: default_profile = Profiles.get_default(light.entity_id) @@ -222,7 +221,6 @@ async def async_setup(hass, config): if default_profile is not None: params = {ATTR_PROFILE: default_profile} preprocess_turn_on_alternatives(params) - turn_light_off, off_params = preprocess_turn_off(params) elif ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params: brightness = light.brightness if light.is_on else 0 @@ -236,13 +234,24 @@ async def async_setup(hass, config): brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255) params[ATTR_BRIGHTNESS] = max(0, min(255, brightness)) - turn_light_off, off_params = preprocess_turn_off(params) + turn_light_off, off_params = preprocess_turn_off(params) if turn_light_off: await light.async_turn_off(**off_params) else: await light.async_turn_on(**params) + async def async_handle_toggle_service(light, call): + """Handle toggling a light. + + If brightness is set to 0, this service will turn the light off. + """ + if light.is_on: + off_params = filter_turn_off_params(call.data["params"]) + await light.async_turn_off(**off_params) + else: + await async_handle_light_on_service(light, call) + # Listen for light on and light off service calls. component.async_register_entity_service( @@ -258,7 +267,9 @@ async def async_setup(hass, config): ) component.async_register_entity_service( - SERVICE_TOGGLE, LIGHT_TURN_ON_SCHEMA, "async_toggle" + SERVICE_TOGGLE, + vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data), + async_handle_toggle_service, ) return True @@ -332,7 +343,7 @@ class Profiles: return None -class Light(ToggleEntity): +class LightEntity(ToggleEntity): """Representation of a light.""" @property @@ -428,3 +439,14 @@ class Light(ToggleEntity): def supported_features(self): """Flag supported features.""" return 0 + + +class Light(LightEntity): + """Representation of a light (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "Light is deprecated, modify %s to extend LightEntity", cls.__name__, + ) diff --git a/homeassistant/components/light/translations/es-419.json b/homeassistant/components/light/translations/es-419.json index a36bd06e27e..f8939b689cf 100644 --- a/homeassistant/components/light/translations/es-419.json +++ b/homeassistant/components/light/translations/es-419.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "brightness_decrease": "Disminuya el brillo de {entity_name}", + "brightness_increase": "Aumenta el brillo de {entity_name}", + "flash": "Destello {entity_name}", + "toggle": "Alternar {entity_name}", + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagada", "is_on": "{entity_name} est\u00e1 encendida" diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index 33db7fd7506..fb6f7f72c9e 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Diminue la luminosit\u00e9 de {entity_name}", + "brightness_increase": "Augmentez la luminosit\u00e9 de {entity_name}", + "flash": "Flash {entity_name}", "toggle": "Basculer {entity_name}", "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" diff --git a/homeassistant/components/light/translations/it.json b/homeassistant/components/light/translations/it.json index c9cc397211d..9477171c9f6 100644 --- a/homeassistant/components/light/translations/it.json +++ b/homeassistant/components/light/translations/it.json @@ -19,8 +19,8 @@ }, "state": { "_": { - "off": "Spento", - "on": "Acceso" + "off": "Spenta", + "on": "Accesa" } }, "title": "Luce" diff --git a/homeassistant/components/light/translations/ko.json b/homeassistant/components/light/translations/ko.json index 969d9b2e4d4..33afda2d06d 100644 --- a/homeassistant/components/light/translations/ko.json +++ b/homeassistant/components/light/translations/ko.json @@ -23,5 +23,5 @@ "on": "\ucf1c\uc9d0" } }, - "title": "\uc804\ub4f1" + "title": "\uc870\uba85" } \ No newline at end of file diff --git a/homeassistant/components/light/translations/nl.json b/homeassistant/components/light/translations/nl.json index 761dd2bdc00..190ed3f52bd 100644 --- a/homeassistant/components/light/translations/nl.json +++ b/homeassistant/components/light/translations/nl.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Verlaag de helderheid van {entity_name}", + "brightness_increase": "Verhoog de helderheid van {entity_name}", + "flash": "Flash {entity_name}", "toggle": "Omschakelen {entity_name}", "turn_off": "{entity_name} uitschakelen", "turn_on": "{entity_name} inschakelen" diff --git a/homeassistant/components/light/translations/no.json b/homeassistant/components/light/translations/no.json index 8b9d94d18b1..77ac84c4440 100644 --- a/homeassistant/components/light/translations/no.json +++ b/homeassistant/components/light/translations/no.json @@ -17,5 +17,11 @@ "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } }, + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, "title": "Lys" } \ No newline at end of file diff --git a/homeassistant/components/light/translations/pl.json b/homeassistant/components/light/translations/pl.json index 7cbff58c44e..3bb887b8d67 100644 --- a/homeassistant/components/light/translations/pl.json +++ b/homeassistant/components/light/translations/pl.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "zmniejsz jasno\u015b\u0107 {entity_name}", "brightness_increase": "zwi\u0119ksz jasno\u015b\u0107 {entity_name}", + "flash": "b\u0142y\u015bnij {entity_name}", "toggle": "prze\u0142\u0105cz {entity_name}", "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" diff --git a/homeassistant/components/light/translations/ru.json b/homeassistant/components/light/translations/ru.json index d6bab88a671..cc95d2d47ce 100644 --- a/homeassistant/components/light/translations/ru.json +++ b/homeassistant/components/light/translations/ru.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c {entity_name}", "brightness_increase": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c {entity_name}", + "flash": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043c\u0438\u0433\u0430\u043d\u0438\u0435 {entity_name}", "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 8e842624b16..0518e91dda9 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -5,7 +5,7 @@ from homeassistant.components.climate import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice, + ClimateEntity, ) from homeassistant.components.climate.const import CURRENT_HVAC_HEAT, CURRENT_HVAC_OFF from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS @@ -29,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class LightwaveTrv(ClimateDevice): +class LightwaveTrv(ClimateEntity): """Representation of a LightWaveRF TRV.""" def __init__(self, name, device_id, lwlink, serial): diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py index 78e4c43a0e7..d441e80b4fa 100644 --- a/homeassistant/components/lightwave/light.py +++ b/homeassistant/components/lightwave/light.py @@ -1,5 +1,9 @@ """Support for LightwaveRF lights.""" -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from homeassistant.const import CONF_NAME from . import LIGHTWAVE_LINK @@ -22,7 +26,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(lights) -class LWRFLight(Light): +class LWRFLight(LightEntity): """Representation of a LightWaveRF light.""" def __init__(self, name, device_id, lwlink): diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py index 16c2aa53462..7fa075a0834 100644 --- a/homeassistant/components/lightwave/switch.py +++ b/homeassistant/components/lightwave/switch.py @@ -1,5 +1,5 @@ """Support for LightwaveRF switches.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME from . import LIGHTWAVE_LINK @@ -20,7 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(switches) -class LWRFSwitch(SwitchDevice): +class LWRFSwitch(SwitchEntity): """Representation of a LightWaveRF switch.""" def __init__(self, name, device_id, lwlink): diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index e0ef635ae87..682d619a92c 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -28,7 +28,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv @@ -200,7 +200,7 @@ def state(new_state): return decorator -class LimitlessLEDGroup(Light, RestoreEntity): +class LimitlessLEDGroup(LightEntity, RestoreEntity): """Representation of a LimitessLED group.""" def __init__(self, group, config): diff --git a/homeassistant/components/linky/strings.json b/homeassistant/components/linky/strings.json index 7770ce3d0ee..dea7062d213 100644 --- a/homeassistant/components/linky/strings.json +++ b/homeassistant/components/linky/strings.json @@ -4,7 +4,10 @@ "user": { "title": "Linky", "description": "Enter your credentials", - "data": { "username": "Email", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -13,6 +16,8 @@ "wrong_login": "Login error: please check your email & password", "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)" }, - "abort": { "already_configured": "Account already configured" } + "abort": { + "already_configured": "Account already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/ca.json b/homeassistant/components/linky/translations/ca.json index 127d2870ae7..954b873083a 100644 --- a/homeassistant/components/linky/translations/ca.json +++ b/homeassistant/components/linky/translations/ca.json @@ -7,7 +7,7 @@ "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet", "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)", "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)", - "wrong_login": "Error d\u2019inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" + "wrong_login": "Error d'inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" }, "step": { "user": { diff --git a/homeassistant/components/linky/translations/es-419.json b/homeassistant/components/linky/translations/es-419.json index ff559803e06..58e44695fc8 100644 --- a/homeassistant/components/linky/translations/es-419.json +++ b/homeassistant/components/linky/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, "error": { "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet.", "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", diff --git a/homeassistant/components/linky/translations/hu.json b/homeassistant/components/linky/translations/hu.json index f5c5f788063..9b450985375 100644 --- a/homeassistant/components/linky/translations/hu.json +++ b/homeassistant/components/linky/translations/hu.json @@ -13,7 +13,8 @@ "data": { "password": "Jelsz\u00f3", "username": "E-mail" - } + }, + "title": "Linky" } } } diff --git a/homeassistant/components/linky/translations/no.json b/homeassistant/components/linky/translations/no.json index b2c565e13df..5cf8ea2da34 100644 --- a/homeassistant/components/linky/translations/no.json +++ b/homeassistant/components/linky/translations/no.json @@ -15,8 +15,8 @@ "password": "Passord", "username": "E-post" }, - "description": "Skriv inn legitimasjonen din", - "title": "Linky" + "description": "Fyll inn legitimasjonen din", + "title": "" } } } diff --git a/homeassistant/components/linky/translations/pl.json b/homeassistant/components/linky/translations/pl.json index 1fc09298fd7..5452a549ec1 100644 --- a/homeassistant/components/linky/translations/pl.json +++ b/homeassistant/components/linky/translations/pl.json @@ -12,8 +12,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%]" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Linky" diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 674a97a0f45..c4a14210d32 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity import homeassistant.helpers.config_validation as cv from . import ( @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class LinodeBinarySensor(BinarySensorDevice): +class LinodeBinarySensor(BinarySensorEntity): """Representation of a Linode droplet sensor.""" def __init__(self, li, node_id): diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index deb561a5e32..c9207ec1be7 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -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 import homeassistant.helpers.config_validation as cv from . import ( @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class LinodeSwitch(SwitchDevice): +class LinodeSwitch(SwitchEntity): """Representation of a Linode Node switch.""" def __init__(self, li, node_id): diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index d99cf27852f..efc6830d775 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -2,7 +2,11 @@ import logging from homeassistant.components import litejet -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) _LOGGER = logging.getLogger(__name__) @@ -21,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class LiteJetLight(Light): +class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" def __init__(self, hass, lj, i, name): diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 7861378d42e..a734dc46d3e 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -2,7 +2,7 @@ import logging from homeassistant.components import litejet -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity ATTR_NUMBER = "number" @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class LiteJetSwitch(SwitchDevice): +class LiteJetSwitch(SwitchEntity): """Representation of a single LiteJet switch.""" def __init__(self, hass, lj, i, name): diff --git a/homeassistant/components/local_ip/translations/es-419.json b/homeassistant/components/local_ip/translations/es-419.json new file mode 100644 index 00000000000..ba120a1ce14 --- /dev/null +++ b/homeassistant/components/local_ip/translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de IP local." + }, + "step": { + "user": { + "data": { + "name": "Nombre del sensor" + }, + "title": "Direcci\u00f3n IP local" + } + } + }, + "title": "Direcci\u00f3n IP local" +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index 88de9704a6e..ba75a9b2a4d 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van lokaal IP toegestaan." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/local_ip/translations/sv.json b/homeassistant/components/local_ip/translations/sv.json index 9c8f27dff8d..d4b508b41a7 100644 --- a/homeassistant/components/local_ip/translations/sv.json +++ b/homeassistant/components/local_ip/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en konfiguration av lokal IP \u00e4r till\u00e5ten." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/locative/translations/ko.json b/homeassistant/components/locative/translations/ko.json index e2e51a3cbd0..73767678df4 100644 --- a/homeassistant/components/locative/translations/ko.json +++ b/homeassistant/components/locative/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "Locative Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Locative Webhook \uc124\uc815" + "description": "Locative \uc6f9 \ud6c5\uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Locative \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 1b79108846f..fb10580a1cc 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -25,6 +25,8 @@ from homeassistant.helpers.entity_component import EntityComponent # mypy: allow-untyped-defs, no-check-untyped-defs +_LOGGER = logging.getLogger(__name__) + ATTR_CHANGED_BY = "changed_by" DOMAIN = "lock" @@ -75,7 +77,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class LockDevice(Entity): +class LockEntity(Entity): """Representation of a lock.""" @property @@ -134,3 +136,14 @@ class LockDevice(Entity): if locked is None: return None return STATE_LOCKED if locked else STATE_UNLOCKED + + +class LockDevice(LockEntity): + """Representation of a lock (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "LockDevice is deprecated, modify %s to extend LockEntity", cls.__name__, + ) diff --git a/homeassistant/components/lock/translations/es-419.json b/homeassistant/components/lock/translations/es-419.json index ff9210f96fd..09b0932a014 100644 --- a/homeassistant/components/lock/translations/es-419.json +++ b/homeassistant/components/lock/translations/es-419.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + }, + "trigger_type": { + "locked": "{entity_name} bloqueado", + "unlocked": "{entity_name} desbloqueado" + } + }, "state": { "_": { "locked": "Cerrado", diff --git a/homeassistant/components/lock/translations/no.json b/homeassistant/components/lock/translations/no.json index a28de395d01..1b804a93414 100644 --- a/homeassistant/components/lock/translations/no.json +++ b/homeassistant/components/lock/translations/no.json @@ -14,5 +14,11 @@ "unlocked": "{entity_name} l\u00e5st opp" } }, + "state": { + "_": { + "locked": "L\u00e5st", + "unlocked": "Ul\u00e5st" + } + }, "title": "L\u00e5s" } \ No newline at end of file diff --git a/homeassistant/components/lockitron/lock.py b/homeassistant/components/lockitron/lock.py index 7d34bb02472..e1ece3da725 100644 --- a/homeassistant/components/lockitron/lock.py +++ b/homeassistant/components/lockitron/lock.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice +from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ID, HTTP_OK import homeassistant.helpers.config_validation as cv @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Error retrieving lock status during init: %s", response.text) -class Lockitron(LockDevice): +class Lockitron(LockEntity): """Representation of a Lockitron lock.""" LOCK_STATE = "lock" diff --git a/homeassistant/components/logi_circle/strings.json b/homeassistant/components/logi_circle/strings.json index 347589c7881..d96f0e041a1 100644 --- a/homeassistant/components/logi_circle/strings.json +++ b/homeassistant/components/logi_circle/strings.json @@ -8,7 +8,7 @@ }, "auth": { "title": "Authenticate with Logi Circle", - "description": "Please follow the link below and Accept access to your Logi Circle account, then come back and press Submit below.\n\n[Link]({authorization_url})" + "description": "Please follow the link below and **Accept** access to your Logi Circle account, then come back and press **Submit** below.\n\n[Link]({authorization_url})" } }, "create_entry": { diff --git a/homeassistant/components/logi_circle/translations/ca.json b/homeassistant/components/logi_circle/translations/ca.json index 2da6bc19d7d..8b81f752058 100644 --- a/homeassistant/components/logi_circle/translations/ca.json +++ b/homeassistant/components/logi_circle/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte de Logi Circule.", - "external_error": "S'ha produ\u00eft una excepci\u00f3 d\u2019un altre flux de dades.", + "external_error": "S'ha produ\u00eft una excepci\u00f3 d'un altre flux de dades.", "external_setup": "Logi Circle s'ha configurat correctament des d'un altre flux de dades.", "no_flows": "Necessites configurar Logi Circle abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/logi_circle/)." }, @@ -10,8 +10,8 @@ "default": "Autenticaci\u00f3 exitosa amb Logi Circle." }, "error": { - "auth_error": "Ha fallat l\u2019autoritzaci\u00f3 de l\u2019API.", - "auth_timeout": "L\u2019autoritzaci\u00f3 ha expirat durant l'obtenci\u00f3 del testimoni d\u2019acc\u00e9s.", + "auth_error": "Ha fallat l'autoritzaci\u00f3 de l'API.", + "auth_timeout": "L'autoritzaci\u00f3 ha expirat durant l'obtenci\u00f3 del token d'acc\u00e9s.", "follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia" }, "step": { diff --git a/homeassistant/components/logi_circle/translations/es-419.json b/homeassistant/components/logi_circle/translations/es-419.json index 8cc68833875..4f512189b32 100644 --- a/homeassistant/components/logi_circle/translations/es-419.json +++ b/homeassistant/components/logi_circle/translations/es-419.json @@ -3,22 +3,27 @@ "abort": { "already_setup": "Solo puede configurar una sola cuenta de Logi Circle.", "external_error": "Se produjo una excepci\u00f3n de otro flujo.", - "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo." + "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo.", + "no_flows": "Debe configurar Logi Circle antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Autenticado con \u00e9xito con Logi Circle." }, "error": { - "auth_error": "Autorizaci\u00f3n de API fallida." + "auth_error": "Autorizaci\u00f3n de API fallida.", + "auth_timeout": "La autorizaci\u00f3n agot\u00f3 el tiempo de espera al solicitar el token de acceso.", + "follow_link": "Siga el enlace y autent\u00edquese antes de presionar Enviar." }, "step": { "auth": { + "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceda a su cuenta de Logi Circle, luego regrese y presione Enviar continuaci\u00f3n. \n\n[Enlace] ({authorization_url})", "title": "Autenticar con Logi Circle" }, "user": { "data": { "flow_impl": "Proveedor" }, + "description": "Elija a trav\u00e9s del proveedor de autenticaci\u00f3n que desea autenticar con Logi Circle.", "title": "Proveedor de autenticaci\u00f3n" } } diff --git a/homeassistant/components/logi_circle/translations/fi.json b/homeassistant/components/logi_circle/translations/fi.json new file mode 100644 index 00000000000..8b7c30df298 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "flow_impl": "Tarjoaja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json index 5ecea7db659..cb3e2aab323 100644 --- a/homeassistant/components/logi_circle/translations/ko.json +++ b/homeassistant/components/logi_circle/translations/ko.json @@ -17,7 +17,7 @@ "step": { "auth": { "description": "\uc544\ub798 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud558\uc5ec Logi Circle \uacc4\uc815\uc5d0 \ub300\ud574 \ub3d9\uc758 \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n[\ub9c1\ud06c]({authorization_url})", - "title": "Logi Circle \uc778\uc99d" + "title": "Logi Circle \uc778\uc99d\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/logi_circle/translations/no.json b/homeassistant/components/logi_circle/translations/no.json index 2a274bf75d7..7c0cae590cd 100644 --- a/homeassistant/components/logi_circle/translations/no.json +++ b/homeassistant/components/logi_circle/translations/no.json @@ -4,14 +4,14 @@ "already_setup": "Du kan bare konfigurere en Logi Circle konto.", "external_error": "Det oppstod et unntak fra en annen flow.", "external_setup": "Logi Circle er vellykket konfigurert fra en annen flow.", - "no_flows": "Du m\u00e5 konfigurere Logi Circle f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "Du m\u00e5 konfigurere Logi Circle f\u00f8r du kan godkjenne den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { - "default": "Vellykket autentisering med Logi Circle" + "default": "Vellykket godkjenning med Logi Circle" }, "error": { - "auth_error": "API-autorisasjonen mislyktes.", - "auth_timeout": "Autorisasjon ble tidsavbrutt da du ba om token.", + "auth_error": "API-godkjenning mislyktes.", + "auth_timeout": "Godkjenningen ble tidsavbrutt ved foresp\u00f8rsel om token.", "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send." }, "step": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "Tilbyder" }, - "description": "Velg med hvilken autentiseringsleverand\u00f8r du vil godkjenne Logi Circle.", - "title": "Autentiseringsleverand\u00f8r" + "description": "Velg med hvilken godkjenningsleverand\u00f8r du vil godkjenne Logi Circle.", + "title": "Godkjenningsleverand\u00f8r" } } } diff --git a/homeassistant/components/lovelace/translations/no.json b/homeassistant/components/lovelace/translations/no.json index 2fc0c81a46c..d8a4c453015 100644 --- a/homeassistant/components/lovelace/translations/no.json +++ b/homeassistant/components/lovelace/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Lovelace" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/fi.json b/homeassistant/components/luftdaten/translations/fi.json new file mode 100644 index 00000000000..452cda748f7 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "show_on_map": "N\u00e4yt\u00e4 kartalla", + "station_id": "Luftdaten-anturin ID" + }, + "title": "M\u00e4\u00e4rit\u00e4 Luftdaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/ko.json b/homeassistant/components/luftdaten/translations/ko.json index a5b4c5cc466..42f3d80f880 100644 --- a/homeassistant/components/luftdaten/translations/ko.json +++ b/homeassistant/components/luftdaten/translations/ko.json @@ -11,7 +11,7 @@ "show_on_map": "\uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ud558\uae30", "station_id": "Luftdaten \uc13c\uc11c ID" }, - "title": "Luftdaten \uc124\uc815" + "title": "Luftdaten \uc815\uc758\ud558\uae30" } } } diff --git a/homeassistant/components/luftdaten/translations/no.json b/homeassistant/components/luftdaten/translations/no.json index 1d48834f755..8c1b69bed07 100644 --- a/homeassistant/components/luftdaten/translations/no.json +++ b/homeassistant/components/luftdaten/translations/no.json @@ -11,7 +11,7 @@ "show_on_map": "Vis p\u00e5 kart", "station_id": "Luftdaten Sensor ID" }, - "title": "Definer Luftdaten" + "title": "" } } } diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index c6ad817bfbf..78f476be751 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -1,7 +1,7 @@ """Support for Lupusec System alarm control panels.""" from datetime import timedelta -from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(alarm_devices) -class LupusecAlarm(LupusecDevice, AlarmControlPanel): +class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): """An alarm_control_panel implementation for Lupusec.""" @property diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index b2a332a03e7..30af5743aa0 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -4,7 +4,7 @@ import logging import lupupy.constants as CONST -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class LupusecBinarySensor(LupusecDevice, BinarySensorDevice): +class LupusecBinarySensor(LupusecDevice, BinarySensorEntity): """A binary sensor implementation for Lupusec device.""" @property diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index a6864f39ef7..d18fcd7c1f7 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -4,7 +4,7 @@ import logging import lupupy.constants as CONST -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class LupusecSwitch(LupusecDevice, SwitchDevice): +class LupusecSwitch(LupusecDevice, SwitchEntity): """Representation of a Lupusec switch.""" def turn_on(self, **kwargs): diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index 866c82a7b2a..e2e143da435 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -3,7 +3,7 @@ from pylutron import OccupancyGroup from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, - BinarySensorDevice, + BinarySensorEntity, ) from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class LutronOccupancySensor(LutronDevice, BinarySensorDevice): +class LutronOccupancySensor(LutronDevice, BinarySensorEntity): """Representation of a Lutron Occupancy Group. The Lutron integration API reports "occupancy groups" rather than diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index cf8d4d16427..438a433fb0f 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -6,7 +6,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, - CoverDevice, + CoverEntity, ) from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class LutronCover(LutronDevice, CoverDevice): +class LutronCover(LutronDevice, CoverEntity): """Representation of a Lutron shade.""" @property diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 938132259d9..2b5bff7d848 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -1,7 +1,11 @@ """Support for Lutron lights.""" import logging -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice @@ -28,7 +32,7 @@ def to_hass_level(level): return int((level * 255) / 100) -class LutronLight(LutronDevice, Light): +class LutronLight(LutronDevice, LightEntity): """Representation of a Lutron Light, including dimmable.""" def __init__(self, area_name, lutron_device, controller): diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 64ca5e6f216..d03cb4a1953 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -1,7 +1,7 @@ """Support for Lutron switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs, True) -class LutronSwitch(LutronDevice, SwitchDevice): +class LutronSwitch(LutronDevice, SwitchEntity): """Representation of a Lutron Switch.""" def __init__(self, area_name, lutron_device, controller): @@ -63,7 +63,7 @@ class LutronSwitch(LutronDevice, SwitchDevice): self._prev_state = self._lutron_device.level > 0 -class LutronLed(LutronDevice, SwitchDevice): +class LutronLed(LutronDevice, SwitchEntity): """Representation of a Lutron Keypad LED.""" def __init__(self, area_name, keypad_name, scene_device, led_device, controller): diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 47df6a221dd..59fd81e650e 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -4,30 +4,30 @@ import logging from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol +from homeassistant import config_entries from homeassistant.const import CONF_HOST -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from .const import CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE + _LOGGER = logging.getLogger(__name__) -LUTRON_CASETA_SMARTBRIDGE = "lutron_smartbridge" - DOMAIN = "lutron_caseta" - -CONF_KEYFILE = "keyfile" -CONF_CERTFILE = "certfile" -CONF_CA_CERTS = "ca_certs" +DATA_BRIDGE_CONFIG = "lutron_caseta_bridges" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_KEYFILE): cv.string, - vol.Required(CONF_CERTFILE): cv.string, - vol.Required(CONF_CA_CERTS): cv.string, - } + DOMAIN: vol.All( + cv.ensure_list, + [ + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_KEYFILE): cv.string, + vol.Required(CONF_CERTFILE): cv.string, + vol.Required(CONF_CA_CERTS): cv.string, + } + ], ) }, extra=vol.ALLOW_EXTRA, @@ -39,29 +39,57 @@ LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_ async def async_setup(hass, base_config): """Set up the Lutron component.""" - config = base_config.get(DOMAIN) - keyfile = hass.config.path(config[CONF_KEYFILE]) - certfile = hass.config.path(config[CONF_CERTFILE]) - ca_certs = hass.config.path(config[CONF_CA_CERTS]) - bridge = Smartbridge.create_tls( - hostname=config[CONF_HOST], - keyfile=keyfile, - certfile=certfile, - ca_certs=ca_certs, - ) - hass.data[LUTRON_CASETA_SMARTBRIDGE] = bridge - await bridge.connect() - if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected(): - _LOGGER.error( - "Unable to connect to Lutron smartbridge at %s", config[CONF_HOST] + bridge_configs = base_config.get(DOMAIN) + + if not bridge_configs: + return True + + hass.data.setdefault(DOMAIN, {}) + + for config in bridge_configs: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + # extract the config keys one-by-one just to be explicit + data={ + CONF_HOST: config[CONF_HOST], + CONF_KEYFILE: config[CONF_KEYFILE], + CONF_CERTFILE: config[CONF_CERTFILE], + CONF_CA_CERTS: config[CONF_CA_CERTS], + }, + ) ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up a bridge from a config entry.""" + + host = config_entry.data[CONF_HOST] + keyfile = config_entry.data[CONF_KEYFILE] + certfile = config_entry.data[CONF_CERTFILE] + ca_certs = config_entry.data[CONF_CA_CERTS] + + bridge = Smartbridge.create_tls( + hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs + ) + + await bridge.connect() + if not bridge.is_connected(): + _LOGGER.error("Unable to connect to Lutron Caseta bridge at %s", host) return False - _LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST]) + _LOGGER.debug("Connected to Lutron Caseta bridge at %s", host) + + # Store this bridge (keyed by entry_id) so it can be retrieved by the + # components we're setting up. + hass.data[DOMAIN][config_entry.entry_id] = bridge for component in LUTRON_CASETA_COMPONENTS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, config) + hass.config_entries.async_forward_entry_setup(config_entry, component) ) return True diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 871f3c28664..4295e3bb367 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -3,17 +3,23 @@ from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, - BinarySensorDevice, + BinarySensorEntity, ) -from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Lutron Caseta lights.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Lutron Caseta binary_sensor platform. + + Adds occupancy groups from the Caseta bridge associated with the + config_entry as binary_sensor entities. + """ + entities = [] - bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] occupancy_groups = bridge.occupancy_groups + for occupancy_group in occupancy_groups.values(): entity = LutronOccupancySensor(occupancy_group, bridge) entities.append(entity) @@ -21,7 +27,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities, True) -class LutronOccupancySensor(LutronCasetaDevice, BinarySensorDevice): +class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Representation of a Lutron occupancy group.""" @property diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py new file mode 100644 index 00000000000..45a7f10fbf0 --- /dev/null +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -0,0 +1,107 @@ +"""Config flow for Lutron Caseta.""" +import logging + +from pylutron_caseta.smartbridge import Smartbridge + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST + +from . import DOMAIN # pylint: disable=unused-import +from .const import ( + ABORT_REASON_ALREADY_CONFIGURED, + ABORT_REASON_CANNOT_CONNECT, + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, + ERROR_CANNOT_CONNECT, + STEP_IMPORT_FAILED, +) + +_LOGGER = logging.getLogger(__name__) + +ENTRY_DEFAULT_TITLE = "Caséta bridge" + + +class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle Lutron Caseta config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize a Lutron Caseta flow.""" + self.data = {} + + async def async_step_import(self, import_info): + """Import a new Caseta bridge as a config entry. + + This flow is triggered by `async_setup`. + """ + + # Abort if existing entry with matching host exists. + host = import_info[CONF_HOST] + if any( + host == entry.data[CONF_HOST] for entry in self._async_current_entries() + ): + return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED) + + # Store the imported config for other steps in this flow to access. + self.data[CONF_HOST] = host + self.data[CONF_KEYFILE] = import_info[CONF_KEYFILE] + self.data[CONF_CERTFILE] = import_info[CONF_CERTFILE] + self.data[CONF_CA_CERTS] = import_info[CONF_CA_CERTS] + + if not await self.async_validate_connectable_bridge_config(): + # Ultimately we won't have a dedicated step for import failure, but + # in order to keep configuration.yaml-based configs transparently + # working without requiring further actions from the user, we don't + # display a form at all before creating a config entry in the + # default case, so we're only going to show a form in case the + # import fails. + # This will change in an upcoming release where UI-based config flow + # will become the default for the Lutron Caseta integration (which + # will require users to go through a confirmation flow for imports). + return await self.async_step_import_failed() + + return self.async_create_entry(title=ENTRY_DEFAULT_TITLE, data=self.data) + + async def async_step_import_failed(self, user_input=None): + """Make failed import surfaced to user.""" + + if user_input is None: + return self.async_show_form( + step_id=STEP_IMPORT_FAILED, + description_placeholders={"host": self.data[CONF_HOST]}, + errors={"base": ERROR_CANNOT_CONNECT}, + ) + + return self.async_abort(reason=ABORT_REASON_CANNOT_CONNECT) + + async def async_validate_connectable_bridge_config(self): + """Check if we can connect to the bridge with the current config.""" + + try: + bridge = Smartbridge.create_tls( + hostname=self.data[CONF_HOST], + keyfile=self.data[CONF_KEYFILE], + certfile=self.data[CONF_CERTFILE], + ca_certs=self.data[CONF_CA_CERTS], + ) + + await bridge.connect() + if not bridge.is_connected(): + return False + + await bridge.close() + return True + except (KeyError, ValueError): + _LOGGER.error( + "Error while checking connectivity to bridge %s", self.data[CONF_HOST], + ) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown exception while checking connectivity to bridge %s", + self.data[CONF_HOST], + ) + return False diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py new file mode 100644 index 00000000000..11bc8bcd6fb --- /dev/null +++ b/homeassistant/components/lutron_caseta/const.py @@ -0,0 +1,10 @@ +"""Lutron Caseta constants.""" + +CONF_KEYFILE = "keyfile" +CONF_CERTFILE = "certfile" +CONF_CA_CERTS = "ca_certs" + +STEP_IMPORT_FAILED = "import_failed" +ERROR_CANNOT_CONNECT = "cannot_connect" +ABORT_REASON_CANNOT_CONNECT = "cannot_connect" +ABORT_REASON_ALREADY_CONFIGURED = "already_configured" diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 60c723b7b42..81a65786900 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -7,19 +7,25 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, - CoverDevice, + CoverEntity, ) -from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Lutron Caseta shades as a cover device.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Lutron Caseta cover platform. + + Adds shades from the Caseta bridge associated with the config_entry as + cover entities. + """ + entities = [] - bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] cover_devices = bridge.get_devices_by_domain(DOMAIN) + for cover_device in cover_devices: entity = LutronCasetaCover(cover_device, bridge) entities.append(entity) @@ -27,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities, True) -class LutronCasetaCover(LutronCasetaDevice, CoverDevice): +class LutronCasetaCover(LutronCasetaDevice, CoverEntity): """Representation of a Lutron shade.""" @property diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 1227371ac07..aa6ab1c7144 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -13,7 +13,7 @@ from homeassistant.components.fan import ( FanEntity, ) -from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice _LOGGER = logging.getLogger(__name__) @@ -36,10 +36,15 @@ SPEED_TO_VALUE = { FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Lutron fan.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Lutron Caseta fan platform. + + Adds fan controllers from the Caseta bridge associated with the config_entry + as fan entities. + """ + entities = [] - bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] fan_devices = bridge.get_devices_by_domain(DOMAIN) for fan_device in fan_devices: diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index 350c35fffa8..471be51219b 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -5,10 +5,10 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, DOMAIN, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) -from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice _LOGGER = logging.getLogger(__name__) @@ -23,11 +23,17 @@ def to_hass_level(level): return int((level * 255) // 100) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Lutron Caseta lights.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Lutron Caseta light platform. + + Adds dimmers from the Caseta bridge associated with the config_entry as + light entities. + """ + entities = [] - bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] light_devices = bridge.get_devices_by_domain(DOMAIN) + for light_device in light_devices: entity = LutronCasetaLight(light_device, bridge) entities.append(entity) @@ -35,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities, True) -class LutronCasetaLight(LutronCasetaDevice, Light): +class LutronCasetaLight(LutronCasetaDevice, LightEntity): """Representation of a Lutron Light, including dimmable.""" @property diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index e3b74d8157b..7b55dfd9c87 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -3,5 +3,6 @@ "name": "Lutron Caséta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": ["pylutron-caseta==0.6.1"], - "codeowners": ["@swails"] -} + "codeowners": ["@swails"], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 8b4dc466c90..c74f60bc88c 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -4,16 +4,22 @@ from typing import Any from homeassistant.components.scene import Scene -from . import LUTRON_CASETA_SMARTBRIDGE +from . import DOMAIN as CASETA_DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Lutron Caseta lights.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Lutron Caseta scene platform. + + Adds scenes from the Caseta bridge associated with the config_entry as + scene entities. + """ + entities = [] - bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] scenes = bridge.get_scenes() + for scene in scenes: entity = LutronCasetaScene(scenes[scene], bridge) entities.append(entity) diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json new file mode 100644 index 00000000000..082497b1bf2 --- /dev/null +++ b/homeassistant/components/lutron_caseta/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "import_failed": { + "title": "Failed to import Caséta bridge configuration.", + "description": "Couldn’t setup bridge (host: {host}) imported from configuration.yaml." + } + }, + "error": { + "cannot_connect": "Failed to connect to Caséta bridge; check your host and certificate configuration." + }, + "abort": { + "already_configured": "Caséta bridge already configured.", + "cannot_connect": "Cancelled setup of Caséta bridge due to connection failure." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 23cd1db8f79..d7f9246feeb 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -1,17 +1,22 @@ """Support for Lutron Caseta switches.""" import logging -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity -from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Lutron switch.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Lutron Caseta switch platform. + + Adds switches from the Caseta bridge associated with the config_entry as + switch entities. + """ + entities = [] - bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] switch_devices = bridge.get_devices_by_domain(DOMAIN) for switch_device in switch_devices: @@ -22,7 +27,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return True -class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): +class LutronCasetaLight(LutronCasetaDevice, SwitchEntity): """Representation of a Lutron Caseta switch.""" async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index 970d722fe4c..40ad8a42f82 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "L'enlla\u00e7 de Cas\u00e9ta ja configurat.", + "cannot_connect": "S'ha cancel\u00b7lat la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta per un error en la connexi\u00f3." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar a l'enlla\u00e7 de Cas\u00e9ta; comprova la configuraci\u00f3 de l'amfitri\u00f3 i del certificat." + }, + "step": { + "import_failed": { + "description": "No s'ha pogut configurar l'enlla\u00e7 (amfitri\u00f3: {host}) importat de configuration.yaml.", + "title": "No s'ha pogut importar la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta." + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index 970d722fe4c..5397985b936 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "Cas\u00e9ta bridge already configured.", + "cannot_connect": "Cancelled setup of Cas\u00e9ta bridge due to connection failure." + }, + "error": { + "cannot_connect": "Failed to connect to Cas\u00e9ta bridge; check your host and certificate configuration." + }, + "step": { + "import_failed": { + "description": "Couldn\u2019t setup bridge (host: {host}) imported from configuration.yaml.", + "title": "Failed to import Cas\u00e9ta bridge configuration." + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es-419.json b/homeassistant/components/lutron_caseta/translations/es-419.json new file mode 100644 index 00000000000..970d722fe4c --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/es-419.json @@ -0,0 +1,3 @@ +{ + "title": "Lutron Cas\u00e9ta" +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index 970d722fe4c..db3a46f3221 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "El bridge Cas\u00e9ta ya est\u00e1 configurado.", + "cannot_connect": "Configuraci\u00f3n cancelada para el bridge Cas\u00e9ta debido a un error en la conexi\u00f3n." + }, + "error": { + "cannot_connect": "No se pudo conectar con el bridge Cas\u00e9ta; compruebe la configuraci\u00f3n del host y del certificado." + }, + "step": { + "import_failed": { + "description": "No se puede configurar bridge (host: {host}) importado desde configuration.yaml.", + "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/fi.json b/homeassistant/components/lutron_caseta/translations/fi.json new file mode 100644 index 00000000000..1dc9bd106af --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Cas\u00e9ta-silta on jo konfiguroitu." + }, + "step": { + "import_failed": { + "title": "Cas\u00e9ta-sillan asetuksien tuonti ep\u00e4onnistui." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json index 970d722fe4c..a73e72fa1ae 100644 --- a/homeassistant/components/lutron_caseta/translations/it.json +++ b/homeassistant/components/lutron_caseta/translations/it.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "Il bridge Cas\u00e9ta \u00e8 gi\u00e0 configurato.", + "cannot_connect": "Configurazione annullata del bridge Cas\u00e9ta a causa di un errore di connessione." + }, + "error": { + "cannot_connect": "Impossibile connettersi al bridge Cas\u00e9ta; controllare la configurazione dell'host e del certificato." + }, + "step": { + "import_failed": { + "description": "Impossibile impostare il bridge (host: {host}) importato da configuration.yaml.", + "title": "Impossibile importare la configurazione del bridge Cas\u00e9ta." + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ko.json b/homeassistant/components/lutron_caseta/translations/ko.json index 970d722fe4c..c1578edeed4 100644 --- a/homeassistant/components/lutron_caseta/translations/ko.json +++ b/homeassistant/components/lutron_caseta/translations/ko.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "Cas\u00e9ta \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uc5f0\uacb0 \uc2e4\ud328\ub85c \uc124\uc815\uc774 \ucde8\uc18c\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "Cas\u00e9ta \ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \ubc0f \uc778\uc99d\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "import_failed": { + "description": "configuration.yaml \uc5d0\uc11c \uac00\uc838\uc628 \ube0c\ub9ac\uc9c0 (\ud638\uc2a4\ud2b8:{host}) \ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "title": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uad6c\uc131\uc744 \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/lb.json b/homeassistant/components/lutron_caseta/translations/lb.json index 970d722fe4c..90549b6f464 100644 --- a/homeassistant/components/lutron_caseta/translations/lb.json +++ b/homeassistant/components/lutron_caseta/translations/lb.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "Cas\u00e9ta Bridge ass schon konfigur\u00e9iert.", + "cannot_connect": "Ariichten vun der Cas\u00e9ta Bridge ofgebrach w\u00e9inst engem Problem mat der Verbindung." + }, + "error": { + "cannot_connect": "Feeler beim verbanne mat der Cas\u00e9ta Bridge; iwwerpr\u00e9if den Numm an Zertifikat" + }, + "step": { + "import_failed": { + "description": "Konnt Bridge (host: {host}) net arrichten anhand vum import vun der configuration.yaml.", + "title": "Feeler beim import vun der Cas\u00e9ta Bridge Konfiguratioun" + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index 970d722fe4c..5d09a7bf9a1 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -1,3 +1,18 @@ { - "title": "Lutron Cas\u00e9ta" + "config": { + "abort": { + "already_configured": "Cas\u00e9ta bridge allerede konfigurert.", + "cannot_connect": "Avbrutt oppsett av Cas\u00e9ta bridge p\u00e5 grunn av tilkoblingssvikt." + }, + "error": { + "cannot_connect": "Kunne ikke koble til Cas\u00e9ta bridge; sjekk verts- og sertifikatkonfigurasjonen." + }, + "step": { + "import_failed": { + "description": "Kunne ikke konfigurere bridge (host: {host} ) importert fra configuration.yaml.", + "title": "Kan ikke importere Cas\u00e9ta bridge-konfigurasjon." + } + } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index 970d722fe4c..9b2b56a70e7 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -1,3 +1,18 @@ { + "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.", + "cannot_connect": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u0437-\u0437\u0430 \u0441\u0431\u043e\u044f \u0432 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0438." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442." + }, + "step": { + "import_failed": { + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml (\u0445\u043e\u0441\u0442: {host}).", + "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0448\u043b\u044e\u0437\u0430." + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index 970d722fe4c..ae46bc41258 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -1,3 +1,18 @@ { + "config": { + "abort": { + "already_configured": "Cas\u00e9ta \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "cannot_connect": "\u7531\u65bc\u9023\u7dda\u5931\u6557\uff0c\u5df2\u53d6\u6d88 Cas\u00e9ta bridge \u8a2d\u5b9a\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u81f3 Cas\u00e9ta bridge \u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u8207\u8a8d\u8b49\u8a2d\u5b9a\u3002" + }, + "step": { + "import_failed": { + "description": "\u7121\u6cd5\u8a2d\u5b9a\u7531 configuration.yaml \u532f\u5165\u7684 bridge\uff08\u4e3b\u6a5f\uff1a{host}\uff09\u3002", + "title": "\u532f\u5165 Cas\u00e9ta bridge \u8a2d\u5b9a\u5931\u6557\u3002" + } + } + }, "title": "Lutron Cas\u00e9ta" } \ No newline at end of file diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index abf75a1e318..907e6b898d6 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_EFFECT, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([LW12WiFi(name, lw12_light)]) -class LW12WiFi(Light): +class LW12WiFi(LightEntity): """LW-12 WiFi LED Controller.""" def __init__(self, name, lw12_light): diff --git a/homeassistant/components/mailgun/translations/ca.json b/homeassistant/components/mailgun/translations/ca.json index eb79eb5eddd..629205b276d 100644 --- a/homeassistant/components/mailgun/translations/ca.json +++ b/homeassistant/components/mailgun/translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/json\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/cs.json b/homeassistant/components/mailgun/translations/cs.json index 7a687ac8e92..110364427d8 100644 --- a/homeassistant/components/mailgun/translations/cs.json +++ b/homeassistant/components/mailgun/translations/cs.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Povolena je pouze jedna instance." }, "create_entry": { - "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [Webhooks with Mailgun]({mailgun_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}' \n - Metoda: POST \n - Typ obsahu: aplikace / json \n\n Viz [dokumentace]({docs_url}), jak konfigurovat automatizace pro zpracov\u00e1n\u00ed p\u0159\u00edchoz\u00edch dat." + "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [Webhooks with Mailgun]({mailgun_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}`\n - Metoda: POST \n - Typ obsahu: application/json\n\n Viz [dokumentace]({docs_url}), jak konfigurovat automatizace pro zpracov\u00e1n\u00ed p\u0159\u00edchoz\u00edch dat." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/da.json b/homeassistant/components/mailgun/translations/da.json index ee50332933f..b0aca0f8737 100644 --- a/homeassistant/components/mailgun/translations/da.json +++ b/homeassistant/components/mailgun/translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/de.json b/homeassistant/components/mailgun/translations/de.json index d37fe5d0388..be795b2f8f5 100644 --- a/homeassistant/components/mailgun/translations/de.json +++ b/homeassistant/components/mailgun/translations/de.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, musst [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." + "default": "Um Ereignisse an den Home Assistant zu senden, musst [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/es-419.json b/homeassistant/components/mailgun/translations/es-419.json index 4e95e6934f8..154e7873644 100644 --- a/homeassistant/components/mailgun/translations/es-419.json +++ b/homeassistant/components/mailgun/translations/es-419.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Solo una instancia es necesaria." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [Webhooks with Mailgun] ( {mailgun_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [Webhooks with Mailgun]({mailgun_url}). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/json\n\n Consulte [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/fr.json b/homeassistant/components/mailgun/translations/fr.json index 84c69d1b6f0..266c48f91a5 100644 --- a/homeassistant/components/mailgun/translations/fr.json +++ b/homeassistant/components/mailgun/translations/fr.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "create_entry": { - "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Mailgun] ( {mailgun_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Mailgun]({mailgun_url}). \n\n Remplissez les informations suivantes: \n\n - URL: `{webhook_url}` \n - M\u00e9thode: POST \n - Type de contenu: application/json \n\n Voir [la documentation]({docs_url}) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/it.json b/homeassistant/components/mailgun/translations/it.json index f913f513b21..624301a14d1 100644 --- a/homeassistant/components/mailgun/translations/it.json +++ b/homeassistant/components/mailgun/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/ko.json b/homeassistant/components/mailgun/translations/ko.json index 975b0154da5..b42dbbf9f2f 100644 --- a/homeassistant/components/mailgun/translations/ko.json +++ b/homeassistant/components/mailgun/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun Webhook]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun \uc6f9 \ud6c5]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "description": "Mailgun \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Mailgun Webhook \uc124\uc815" + "title": "Mailgun \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/mailgun/translations/nl.json b/homeassistant/components/mailgun/translations/nl.json index 9444b5045d3..24d10647e32 100644 --- a/homeassistant/components/mailgun/translations/nl.json +++ b/homeassistant/components/mailgun/translations/nl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Slechts \u00e9\u00e9n enkele instantie is nodig." }, "create_entry": { - "default": "Om evenementen naar Home Assistant te verzenden, moet u [Webhooks with Mailgun] instellen ( {mailgun_url} ). \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhoudstype: application/json \n\n Zie [de documentatie] ( {docs_url} ) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken." + "default": "Om evenementen naar Home Assistant te verzenden, moet u [Webhooks with Mailgun]({mailgun_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhoudstype: application/json \n\n Zie [de documentatie]({docs_url}) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/pl.json b/homeassistant/components/mailgun/translations/pl.json index c1184dbb9e4..e24f2d7ec8a 100644 --- a/homeassistant/components/mailgun/translations/pl.json +++ b/homeassistant/components/mailgun/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/pt-BR.json b/homeassistant/components/mailgun/translations/pt-BR.json index 6285162878e..5fbdd643f07 100644 --- a/homeassistant/components/mailgun/translations/pt-BR.json +++ b/homeassistant/components/mailgun/translations/pt-BR.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." }, "create_entry": { - "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks com Mailgun] ( {mailgun_url} ). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application / json \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." + "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks com Mailgun]({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/pt.json b/homeassistant/components/mailgun/translations/pt.json index e6d7329f52a..a0aeedc62a4 100644 --- a/homeassistant/components/mailgun/translations/pt.json +++ b/homeassistant/components/mailgun/translations/pt.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." }, "create_entry": { - "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar [Webhooks with Mailgun] ({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/x-www-form-urlencoded \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar [Webhooks with Mailgun]({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json\n\n Veja [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/ru.json b/homeassistant/components/mailgun/translations/ru.json index c26b3b93832..1d469c4692d 100644 --- a/homeassistant/components/mailgun/translations/ru.json +++ b/homeassistant/components/mailgun/translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/zh-Hant.json b/homeassistant/components/mailgun/translations/zh-Hant.json index 1d500c180c2..d0a31f6fed9 100644 --- a/homeassistant/components/mailgun/translations/zh-Hant.json +++ b/homeassistant/components/mailgun/translations/zh-Hant.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Mailgun]({mailgun_url}) \u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8a2d\u5b9a [Webhooks with Mailgun]({mailgun_url}) \u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" }, "step": { "user": { diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index b41da2d51bd..4e361b5086c 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -15,21 +15,23 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_TRIGGER, ) from homeassistant.const import ( + CONF_ARMING_TIME, CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME, - CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.restore_state import RestoreEntity @@ -41,8 +43,8 @@ CONF_CODE_TEMPLATE = "code_template" CONF_CODE_ARM_REQUIRED = "code_arm_required" DEFAULT_ALARM_NAME = "HA Alarm" -DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0) -DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) +DEFAULT_DELAY_TIME = datetime.timedelta(seconds=60) +DEFAULT_ARMING_TIME = datetime.timedelta(seconds=60) DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False @@ -59,12 +61,14 @@ SUPPORTED_PRETRIGGER_STATES = [ state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED ] -SUPPORTED_PENDING_STATES = [ - state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED +SUPPORTED_ARMING_STATES = [ + state + for state in SUPPORTED_STATES + if state not in (STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) ] -ATTR_PRE_PENDING_STATE = "pre_pending_state" -ATTR_POST_PENDING_STATE = "post_pending_state" +ATTR_PREVIOUS_STATE = "previous_state" +ATTR_NEXT_STATE = "next_state" def _state_validator(config): @@ -75,9 +79,9 @@ def _state_validator(config): config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME] if CONF_TRIGGER_TIME not in config[state]: config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] - for state in SUPPORTED_PENDING_STATES: - if CONF_PENDING_TIME not in config[state]: - config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] + for state in SUPPORTED_ARMING_STATES: + if CONF_ARMING_TIME not in config[state]: + config[state][CONF_ARMING_TIME] = config[CONF_ARMING_TIME] return config @@ -92,8 +96,8 @@ def _state_schema(state): schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( cv.time_period, cv.positive_timedelta ) - if state in SUPPORTED_PENDING_STATES: - schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( + if state in SUPPORTED_ARMING_STATES: + schema[vol.Optional(CONF_ARMING_TIME)] = vol.All( cv.time_period, cv.positive_timedelta ) return vol.Schema(schema) @@ -110,7 +114,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All( cv.time_period, cv.positive_timedelta ), - vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All( + vol.Optional(CONF_ARMING_TIME, default=DEFAULT_ARMING_TIME): vol.All( cv.time_period, cv.positive_timedelta ), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All( @@ -160,13 +164,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): +class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): """ Representation of an alarm status. - When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for the triggering state's 'delay_time' - plus the triggered state's 'pending_time'. + When armed, will be arming for 'arming_time', after that armed. + When triggered, will be pending for the triggering state's 'delay_time'. After that will be triggered for 'trigger_time', after that we return to the previous state or disarm if `disarm_after_trigger` is true. A trigger_time of zero disables the alarm_trigger service. @@ -204,9 +207,8 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): state: config[state][CONF_TRIGGER_TIME] for state in SUPPORTED_PRETRIGGER_STATES } - self._pending_time_by_state = { - state: config[state][CONF_PENDING_TIME] - for state in SUPPORTED_PENDING_STATES + self._arming_time_by_state = { + state: config[state][CONF_ARMING_TIME] for state in SUPPORTED_ARMING_STATES } @property @@ -234,10 +236,10 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): self._state = self._previous_state return self._state - if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time( + if self._state in SUPPORTED_ARMING_STATES and self._within_arming_time( self._state ): - return STATE_ALARM_PENDING + return STATE_ALARM_ARMING return self._state @@ -255,16 +257,21 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): @property def _active_state(self): """Get the current state.""" - if self.state == STATE_ALARM_PENDING: + if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING): return self._previous_state return self._state + def _arming_time(self, state): + """Get the arming time.""" + return self._arming_time_by_state[state] + def _pending_time(self, state): """Get the pending time.""" - pending_time = self._pending_time_by_state[state] - if state == STATE_ALARM_TRIGGERED: - pending_time += self._delay_time_by_state[self._previous_state] - return pending_time + return self._delay_time_by_state[self._previous_state] + + def _within_arming_time(self, state): + """Get if the action is in the arming time window.""" + return self._state_ts + self._arming_time(state) > dt_util.utcnow() def _within_pending_time(self, state): """Get if the action is in the pending time window.""" @@ -350,22 +357,26 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - pending_time = self._pending_time(state) if state == STATE_ALARM_TRIGGERED: + pending_time = self._pending_time(state) track_point_in_time( - self._hass, self.async_update_ha_state, self._state_ts + pending_time + self._hass, self.async_scheduled_update, self._state_ts + pending_time ) trigger_time = self._trigger_time_by_state[self._previous_state] track_point_in_time( self._hass, - self.async_update_ha_state, + self.async_scheduled_update, self._state_ts + pending_time + trigger_time, ) - elif state in SUPPORTED_PENDING_STATES and pending_time: - track_point_in_time( - self._hass, self.async_update_ha_state, self._state_ts + pending_time - ) + elif state in SUPPORTED_ARMING_STATES: + arming_time = self._arming_time(state) + if arming_time: + track_point_in_time( + self._hass, + self.async_scheduled_update, + self._state_ts + arming_time, + ) def _validate_code(self, code, state): """Validate given code.""" @@ -385,24 +396,32 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): """Return the state attributes.""" state_attr = {} - if self.state == STATE_ALARM_PENDING: - state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state - state_attr[ATTR_POST_PENDING_STATE] = self._state + if self.state == STATE_ALARM_PENDING or self.state == STATE_ALARM_ARMING: + state_attr[ATTR_PREVIOUS_STATE] = self._previous_state + state_attr[ATTR_NEXT_STATE] = self._state return state_attr + @callback + def async_scheduled_update(self, now): + """Update state at a scheduled point in time.""" + self.async_write_ha_state() + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state: if ( - state.state == STATE_ALARM_PENDING + ( + state.state == STATE_ALARM_PENDING + or state.state == STATE_ALARM_ARMING + ) and hasattr(state, "attributes") - and state.attributes["pre_pending_state"] + and state.attributes[ATTR_PREVIOUS_STATE] ): - # If in pending state, we return to the pre_pending_state - self._state = state.attributes["pre_pending_state"] + # If in arming or pending state, we return to the ATTR_PREVIOUS_STATE + self._state = state.attributes[ATTR_PREVIOUS_STATE] self._state_ts = dt_util.utcnow() else: self._state = state.state diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 00a82118ec4..548227173f4 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -184,7 +184,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ManualMQTTAlarm(alarm.AlarmControlPanel): +class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): """ Representation of an alarm status. diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 2670b61b456..b42c96f99c2 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -1,7 +1,7 @@ """Support for MAX! binary sensors via MAX! Cube.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DATA_KEY @@ -24,11 +24,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class MaxCubeShutter(BinarySensorDevice): +class MaxCubeShutter(BinarySensorEntity): """Representation of a MAX! Cube Binary Sensor device.""" def __init__(self, handler, name, rf_address): - """Initialize MAX! Cube BinarySensorDevice.""" + """Initialize MAX! Cube BinarySensorEntity.""" self._name = name self._sensor_type = "window" self._rf_address = rf_address @@ -42,7 +42,7 @@ class MaxCubeShutter(BinarySensorDevice): @property def name(self): - """Return the name of the BinarySensorDevice.""" + """Return the name of the BinarySensorEntity.""" return self._name @property diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 19bbf8bf000..69d9177da5d 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -9,7 +9,7 @@ from maxcube.device import ( MAX_DEVICE_MODE_VACATION, ) -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -72,11 +72,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class MaxCubeClimate(ClimateDevice): - """MAX! Cube ClimateDevice.""" +class MaxCubeClimate(ClimateEntity): + """MAX! Cube ClimateEntity.""" def __init__(self, handler, name, rf_address): - """Initialize MAX! Cube ClimateDevice.""" + """Initialize MAX! Cube ClimateEntity.""" self._name = name self._rf_address = rf_address self._cubehandle = handler @@ -144,14 +144,11 @@ class MaxCubeClimate(ClimateDevice): """Set new target hvac mode.""" device = self._cubehandle.cube.device_by_rf(self._rf_address) temp = device.target_temperature - mode = device.mode + mode = MAX_DEVICE_MODE_MANUAL if hvac_mode == HVAC_MODE_OFF: temp = OFF_TEMPERATURE - mode = MAX_DEVICE_MODE_MANUAL - elif hvac_mode == HVAC_MODE_HEAT: - mode = MAX_DEVICE_MODE_MANUAL - else: + elif hvac_mode != HVAC_MODE_HEAT: # Reset the temperature to a sane value. # Ideally, we should send 0 and the device will set its # temperature according to the schedule. However, current diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index 59f268e657c..8cc50fa9dfa 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -7,7 +7,7 @@ import busio # pylint: disable=import-error import digitalio # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(binary_sensors, True) -class MCP23017BinarySensor(BinarySensorDevice): +class MCP23017BinarySensor(BinarySensorEntity): """Represent a binary sensor that uses MCP23017.""" def __init__(self, name, pin, pull_mode, invert_logic): diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index fd1d2172873..95cb1db65a8 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.03.24"], + "requirements": ["youtube_dl==2020.05.08"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a8c87effb10..0d73c93ec71 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -48,6 +48,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass from .const import ( @@ -337,8 +338,8 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class MediaPlayerDevice(Entity): - """ABC for media player devices.""" +class MediaPlayerEntity(Entity): + """ABC for media player entities.""" _access_token: Optional[str] = None @@ -820,7 +821,7 @@ async def _async_fetch_image(hass, url): cache_maxsize = ENTITY_IMAGE_CACHE[CACHE_MAXSIZE] if urlparse(url).hostname is None: - url = hass.config.api.base_url + url + url = f"{get_url(hass)}{url}" if url not in cache_images: cache_images[url] = {CACHE_LOCK: asyncio.Lock()} @@ -924,3 +925,15 @@ async def websocket_handle_thumbnail(hass, connection, msg): "content": base64.b64encode(data).decode("utf-8"), }, ) + + +class MediaPlayerDevice(MediaPlayerEntity): + """ABC for media player devices (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "MediaPlayerDevice is deprecated, modify %s to extend MediaPlayerEntity", + cls.__name__, + ) diff --git a/homeassistant/components/media_player/translations/es-419.json b/homeassistant/components/media_player/translations/es-419.json index 667d4af550e..518ca4453f0 100644 --- a/homeassistant/components/media_player/translations/es-419.json +++ b/homeassistant/components/media_player/translations/es-419.json @@ -1,4 +1,13 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e1 inactivo", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "is_paused": "{entity_name} est\u00e1 en pausa", + "is_playing": "{entity_name} est\u00e1 reproduciendo" + } + }, "state": { "_": { "idle": "Inactivo", diff --git a/homeassistant/components/media_player/translations/it.json b/homeassistant/components/media_player/translations/it.json index 989df9c2051..23d1afa0625 100644 --- a/homeassistant/components/media_player/translations/it.json +++ b/homeassistant/components/media_player/translations/it.json @@ -15,7 +15,7 @@ "on": "Acceso", "paused": "In pausa", "playing": "In riproduzione", - "standby": "Pausa" + "standby": "In attesa" } }, "title": "Lettore multimediale" diff --git a/homeassistant/components/media_player/translations/no.json b/homeassistant/components/media_player/translations/no.json index 6358aa9b85a..691ec894a7b 100644 --- a/homeassistant/components/media_player/translations/no.json +++ b/homeassistant/components/media_player/translations/no.json @@ -10,7 +10,12 @@ }, "state": { "_": { - "playing": "Spiller" + "idle": "Inaktiv", + "off": "Av", + "on": "P\u00e5", + "paused": "Pauset", + "playing": "Spiller", + "standby": "Avventer" } }, "title": "Mediaspiller" diff --git a/homeassistant/components/media_player/translations/pl.json b/homeassistant/components/media_player/translations/pl.json index 0c4153bdede..8d4f42a1a61 100644 --- a/homeassistant/components/media_player/translations/pl.json +++ b/homeassistant/components/media_player/translations/pl.json @@ -1,9 +1,9 @@ { "device_automation": { "condition_type": { - "is_idle": "odtwarzacz medi\u00f3w (entity_name} jest nieaktywny", - "is_off": "odtwarzacz medi\u00f3w (entity_name} jest wy\u0142\u0105czony", - "is_on": "odtwarzacz medi\u00f3w (entity_name} jest w\u0142\u0105czony", + "is_idle": "odtwarzacz medi\u00f3w {entity_name} jest nieaktywny", + "is_off": "odtwarzacz medi\u00f3w {entity_name} jest wy\u0142\u0105czony", + "is_on": "odtwarzacz medi\u00f3w {entity_name} jest w\u0142\u0105czony", "is_paused": "odtwarzanie medi\u00f3w na {entity_name} jest wstrzymane", "is_playing": "{entity_name} odtwarza media" } diff --git a/homeassistant/components/media_player/translations/sl.json b/homeassistant/components/media_player/translations/sl.json index f23e746e9c8..6d24f4e923f 100644 --- a/homeassistant/components/media_player/translations/sl.json +++ b/homeassistant/components/media_player/translations/sl.json @@ -14,7 +14,7 @@ "off": "Izklju\u010den", "on": "Vklopljen", "paused": "Na pavzi", - "playing": "Predvaja", + "playing": "Predvajanje", "standby": "V pripravljenosti" } }, diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 492c347959e..abbfa21761f 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -4,7 +4,7 @@ import logging from pymediaroom import PyMediaroomError, Remote, State, install_mediaroom_protocol import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -118,7 +118,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.debug("Auto discovery installed") -class MediaroomDevice(MediaPlayerDevice): +class MediaroomDevice(MediaPlayerEntity): """Representation of a Mediaroom set-up-box on the network.""" def set_state(self, mediaroom_state): diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 9630d1bb855..ed2fefc823d 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -13,7 +13,7 @@ from pymelcloud.atw_device import ( ) import voluptuous as vol -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, @@ -98,7 +98,7 @@ async def async_setup_entry( ) -class MelCloudClimate(ClimateDevice): +class MelCloudClimate(ClimateEntity): """Base climate device.""" def __init__(self, device: MelCloudDevice): diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 4747059345f..aac8db678f9 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,6 +3,6 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.4.1"], + "requirements": ["pymelcloud==2.5.2"], "codeowners": ["@vilppuvuorinen"] } diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 13cc5a94a18..774b66ab67e 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -65,7 +65,23 @@ ATW_ZONE_SENSORS = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda zone: zone.room_temperature, ATTR_ENABLED_FN: lambda x: True, - } + }, + "flow_temperature": { + ATTR_MEASUREMENT_NAME: "Flow Temperature", + ATTR_ICON: "mdi:thermometer", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_VALUE_FN: lambda zone: zone.flow_temperature, + ATTR_ENABLED_FN: lambda x: True, + }, + "return_temperature": { + ATTR_MEASUREMENT_NAME: "Flow Return Temperature", + ATTR_ICON: "mdi:thermometer", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_VALUE_FN: lambda zone: zone.return_temperature, + ATTR_ENABLED_FN: lambda x: True, + }, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index f74398e9443..c4161a87ff8 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -5,8 +5,8 @@ "title": "Connect to MELCloud", "description": "Connect using your MELCloud account.", "data": { - "username": "Email used to login to MELCloud.", - "password": "MELCloud password." + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -19,4 +19,4 @@ "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/ca.json b/homeassistant/components/melcloud/translations/ca.json index f384905d1a4..92472d020c4 100644 --- a/homeassistant/components/melcloud/translations/ca.json +++ b/homeassistant/components/melcloud/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La integraci\u00f3 MELCloud ja est\u00e0 configurada amb aquest correu electr\u00f2nic. El testimoni d'acc\u00e9s s'ha actualitzat." + "already_configured": "La integraci\u00f3 MELCloud ja est\u00e0 configurada amb aquest correu electr\u00f2nic. El token d'acc\u00e9s s'ha actualitzat." }, "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", @@ -14,7 +14,7 @@ "password": "Contrasenya de MELCloud.", "username": "Correu electr\u00f2nic d'inici de sessi\u00f3 a MELCloud." }, - "description": "Connecta\u2019t amb el teu compte de MELCloud.", + "description": "Connecta't amb el teu compte de MELCloud.", "title": "Connexi\u00f3 amb MELCloud" } } diff --git a/homeassistant/components/melcloud/translations/en.json b/homeassistant/components/melcloud/translations/en.json index 4ab1ac566ef..6e701fecf55 100644 --- a/homeassistant/components/melcloud/translations/en.json +++ b/homeassistant/components/melcloud/translations/en.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "MELCloud password.", - "username": "Email used to login to MELCloud." + "password": "Password", + "username": "Email" }, "description": "Connect using your MELCloud account.", "title": "Connect to MELCloud" diff --git a/homeassistant/components/melcloud/translations/es-419.json b/homeassistant/components/melcloud/translations/es-419.json new file mode 100644 index 00000000000..0ee2674c8d9 --- /dev/null +++ b/homeassistant/components/melcloud/translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3n de MELCloud ya configurada para este correo electr\u00f3nico. El token de acceso se ha actualizado." + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a de MELCloud.", + "username": "Correo electr\u00f3nico utilizado para iniciar sesi\u00f3n en MELCloud." + }, + "description": "Con\u00e9ctese usando su cuenta MELCloud.", + "title": "Conectar con MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/fr.json b/homeassistant/components/melcloud/translations/fr.json index 0385113787c..a567058297e 100644 --- a/homeassistant/components/melcloud/translations/fr.json +++ b/homeassistant/components/melcloud/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'int\u00e9gration MELCloud est d\u00e9j\u00e0 configur\u00e9e pour cet e-mail. Le jeton d'acc\u00e8s a \u00e9t\u00e9 actualis\u00e9." + }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "invalid_auth": "Authentification non valide", diff --git a/homeassistant/components/melcloud/translations/ko.json b/homeassistant/components/melcloud/translations/ko.json index 5db3f001b4a..a43d4cfbcb3 100644 --- a/homeassistant/components/melcloud/translations/ko.json +++ b/homeassistant/components/melcloud/translations/ko.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "MELCloud \uc758 \ube44\ubc00\ubc88\ud638\ub97c \ub123\uc5b4\uc8fc\uc138\uc694.", - "username": "MELCloud \ub85c\uadf8\uc778 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \ub123\uc5b4\uc8fc\uc138\uc694." + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" }, "description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.", "title": "MELCloud \uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/melcloud/translations/pl.json b/homeassistant/components/melcloud/translations/pl.json index 070a509cd10..4b6c4d70b80 100644 --- a/homeassistant/components/melcloud/translations/pl.json +++ b/homeassistant/components/melcloud/translations/pl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o MELCloud.", - "username": "Adres e-mail u\u017cywany do logowania do MELCloud" + "password": "[%key_id:common::config_flow::data::password%] MELCloud", + "username": "[%key_id:common::config_flow::data::email%]" }, "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta MELCloud.", "title": "Po\u0142\u0105czenie z MELCloud" diff --git a/homeassistant/components/melcloud/translations/ru.json b/homeassistant/components/melcloud/translations/ru.json index 3b23ca96bfc..7fd6d31a753 100644 --- a/homeassistant/components/melcloud/translations/ru.json +++ b/homeassistant/components/melcloud/translations/ru.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c MELCloud.", - "username": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u044f \u043f\u043e\u0447\u0442\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 MELCloud." + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u0441\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c MELCloud.", "title": "MELCloud" diff --git a/homeassistant/components/melcloud/translations/zh-Hant.json b/homeassistant/components/melcloud/translations/zh-Hant.json index 0702ca3bbd5..1e6e1d880c7 100644 --- a/homeassistant/components/melcloud/translations/zh-Hant.json +++ b/homeassistant/components/melcloud/translations/zh-Hant.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "MELCloud \u5bc6\u78bc\u3002", - "username": "MELCloud \u767b\u5165\u90f5\u4ef6\u3002" + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" }, "description": "\u4f7f\u7528 MELCloud \u5e33\u865f\u9032\u884c\u9023\u7dda\u3002", "title": "\u9023\u7dda\u81f3 MELCloud" diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index ce1b1ae15cc..ae10b5140f7 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -11,7 +11,7 @@ from pymelcloud.device import PROPERTY_POWER from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS @@ -35,7 +35,7 @@ async def async_setup_entry( ) -class AtwWaterHeater(WaterHeaterDevice): +class AtwWaterHeater(WaterHeaterEntity): """Air-to-Water water heater.""" def __init__(self, api: MelCloudDevice, device: AtwDevice) -> None: diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 4b033811f43..abbc15c936f 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -1,7 +1,7 @@ """Support for Melissa Climate A/C.""" import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, FAN_HIGH, @@ -49,7 +49,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(all_devices) -class MelissaClimate(ClimateDevice): +class MelissaClimate(ClimateEntity): """Representation of a Melissa Climate device.""" def __init__(self, api, serial_number, init_data): diff --git a/homeassistant/components/met/translations/fi.json b/homeassistant/components/met/translations/fi.json new file mode 100644 index 00000000000..417b747fff9 --- /dev/null +++ b/homeassistant/components/met/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "elevation": "Korkeus merenpinnasta", + "latitude": "Leveysaste", + "longitude": "Pituusaste", + "name": "Nimi" + }, + "description": "Meteorologisk institutt", + "title": "Sijainti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/no.json b/homeassistant/components/met/translations/no.json index a46b53faf8c..90489288b62 100644 --- a/homeassistant/components/met/translations/no.json +++ b/homeassistant/components/met/translations/no.json @@ -11,7 +11,7 @@ "longitude": "Lengdegrad", "name": "Navn" }, - "description": "Meteorologisk institutt", + "description": "", "title": "Lokasjon" } } diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 572fd29b549..5f12037e011 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -3,6 +3,6 @@ "name": "Météo-France", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", - "requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.0"], + "requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.1"], "codeowners": ["@victorcerutti", "@oncleben31", "@Quentame"] } diff --git a/homeassistant/components/meteo_france/translations/es-419.json b/homeassistant/components/meteo_france/translations/es-419.json new file mode 100644 index 00000000000..471b965a824 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "La ciudad ya est\u00e1 configurada", + "unknown": "Error desconocido: vuelva a intentarlo m\u00e1s tarde" + }, + "step": { + "user": { + "data": { + "city": "Ciudad" + }, + "description": "Ingrese el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad", + "title": "M\u00e9t\u00e9o-Francia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/no.json b/homeassistant/components/meteo_france/translations/no.json index cecc1d75c6a..d4921d7e4e5 100644 --- a/homeassistant/components/meteo_france/translations/no.json +++ b/homeassistant/components/meteo_france/translations/no.json @@ -9,7 +9,7 @@ "data": { "city": "By" }, - "description": "Skriv inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", + "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", "title": "" } } diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index f7e95c50c86..b481b417b9e 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -5,7 +5,7 @@ import logging from meteoalertapi import Meteoalert import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([MeteoAlertBinarySensor(api, name)], True) -class MeteoAlertBinarySensor(BinarySensorDevice): +class MeteoAlertBinarySensor(BinarySensorEntity): """Representation of a MeteoAlert binary sensor.""" def __init__(self, api, name): diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 08ed841e7e1..d2ba2371303 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -5,7 +5,7 @@ from mficlient.client import FailedToLogin, MFiClient import requests 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_HOST, CONF_PASSWORD, @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class MfiSwitch(SwitchDevice): +class MfiSwitch(SwitchEntity): """Representation of an mFi switch-able device.""" def __init__(self, port): diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 16b25e4f85d..208cecf6d3b 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -189,7 +189,9 @@ async def async_setup(hass, config): binary=True, ) except HomeAssistantError as err: - _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) + _LOGGER.error( + "Can't add an image of a person '%s' with error: %s", p_id, err + ) hass.services.async_register( DOMAIN, SERVICE_FACE_PERSON, async_face_person, schema=SCHEMA_FACE_SERVICE diff --git a/homeassistant/components/mikrotik/strings.json b/homeassistant/components/mikrotik/strings.json index 3f4bd769eac..9fa665add80 100644 --- a/homeassistant/components/mikrotik/strings.json +++ b/homeassistant/components/mikrotik/strings.json @@ -5,10 +5,10 @@ "title": "Set up Mikrotik Router", "data": { "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port", + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", "verify_ssl": "Use ssl" } } @@ -18,7 +18,9 @@ "cannot_connect": "Connection Unsuccessful", "wrong_credentials": "Wrong Credentials" }, - "abort": { "already_configured": "Mikrotik is already configured" } + "abort": { + "already_configured": "Mikrotik is already configured" + } }, "options": { "step": { @@ -31,4 +33,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/es-419.json b/homeassistant/components/mikrotik/translations/es-419.json new file mode 100644 index 00000000000..1b464bed393 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/es-419.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Conexi\u00f3n fallida", + "name_exists": "El nombre existe", + "wrong_credentials": "Credenciales incorrectas" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario", + "verify_ssl": "Usar SSL" + }, + "title": "Configurar el Router Mikrotik" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Habilitar ping ARP", + "detection_time": "Considere el intervalo de inicio", + "force_dhcp": "Escaneo forzado utilizando DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/ko.json b/homeassistant/components/mikrotik/translations/ko.json index 8c73521afd5..04efde51ad4 100644 --- a/homeassistant/components/mikrotik/translations/ko.json +++ b/homeassistant/components/mikrotik/translations/ko.json @@ -18,7 +18,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc0ac\uc6a9" }, - "title": "Mikrotik \ub77c\uc6b0\ud130 \uc124\uc815" + "title": "Mikrotik \ub77c\uc6b0\ud130 \uc124\uc815\ud558\uae30" } } }, diff --git a/homeassistant/components/mikrotik/translations/lb.json b/homeassistant/components/mikrotik/translations/lb.json index 2ae18d83422..b3864ad2106 100644 --- a/homeassistant/components/mikrotik/translations/lb.json +++ b/homeassistant/components/mikrotik/translations/lb.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung net erfollegr\u00e4ich", "name_exists": "Numm g\u00ebtt et schonn", - "wrong_credentials": "Falsh Login Informatiounen" + "wrong_credentials": "Falsch Login Informatiounen" }, "step": { "user": { diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json index 6fa745a8b57..894b70cbbe5 100644 --- a/homeassistant/components/mikrotik/translations/no.json +++ b/homeassistant/components/mikrotik/translations/no.json @@ -14,11 +14,11 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "", + "port": "Port", "username": "Brukernavn", "verify_ssl": "Bruk ssl" }, - "title": "Konfigurere Mikrotik-ruter" + "title": "Sett opp Mikrotik-ruter" } } }, diff --git a/homeassistant/components/mikrotik/translations/pl.json b/homeassistant/components/mikrotik/translations/pl.json index 20b0348570c..4dae38268d4 100644 --- a/homeassistant/components/mikrotik/translations/pl.json +++ b/homeassistant/components/mikrotik/translations/pl.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]", + "username": "[%key_id:common::config_flow::data::username%]", "verify_ssl": "U\u017cyj SSL" }, "title": "Konfiguracja routera Mikrotik" diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 157ea345efd..652153f9a8c 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1 +1,25 @@ """The mill component.""" +import logging + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Mill platform.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up the Mill heater.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_forward_entry_unload( + config_entry, "climate" + ) + return unload_ok diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index d904538451c..ee7fc8cffb7 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -4,7 +4,7 @@ import logging from mill import Mill import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -29,6 +30,7 @@ from .const import ( ATTR_ROOM_NAME, ATTR_SLEEP_TEMP, DOMAIN, + MANUFACTURER, MAX_TEMP, MIN_TEMP, SERVICE_SET_ROOM_TEMP, @@ -38,10 +40,6 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - SET_ROOM_TEMP_SCHEMA = vol.Schema( { vol.Required(ATTR_ROOM_NAME): cv.string, @@ -52,16 +50,15 @@ SET_ROOM_TEMP_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Mill heater.""" +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Mill climate.""" mill_data_connection = Mill( - config[CONF_USERNAME], - config[CONF_PASSWORD], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], websession=async_get_clientsession(hass), ) if not await mill_data_connection.connect(): - _LOGGER.error("Failed to connect to Mill") - return + raise ConfigEntryNotReady await mill_data_connection.find_all_heaters() @@ -85,7 +82,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MillHeater(ClimateDevice): +class MillHeater(ClimateEntity): """Representation of a Mill Thermostat device.""" def __init__(self, heater, mill_data_connection): @@ -218,3 +215,19 @@ class MillHeater(ClimateDevice): async def async_update(self): """Retrieve latest state.""" self._heater = await self._conn.update_device(self._heater.device_id) + + @property + def device_id(self): + """Return the ID of the physical device this sensor is part of.""" + return self._heater.device_id + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": f"generation {1 if self._heater.is_gen1 else 2}", + } + return device_info diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py new file mode 100644 index 00000000000..08eb0f5c536 --- /dev/null +++ b/homeassistant/components/mill/config_flow.py @@ -0,0 +1,55 @@ +"""Adds config flow for Mill integration.""" +import logging + +from mill import Mill +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Mill integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors={}, + ) + + username = user_input[CONF_USERNAME].replace(" ", "") + password = user_input[CONF_PASSWORD].replace(" ", "") + + mill_data_connection = Mill( + username, password, websession=async_get_clientsession(self.hass), + ) + + errors = {} + + if not await mill_data_connection.connect(): + errors["connection_error"] = "connection_error" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors, + ) + + unique_id = username + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=unique_id, data={CONF_USERNAME: username, CONF_PASSWORD: password}, + ) diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py index 65c67b72b6e..b0ba7065e0a 100644 --- a/homeassistant/components/mill/const.py +++ b/homeassistant/components/mill/const.py @@ -4,6 +4,7 @@ ATTR_AWAY_TEMP = "away_temp" ATTR_COMFORT_TEMP = "comfort_temp" ATTR_ROOM_NAME = "room_name" ATTR_SLEEP_TEMP = "sleep_temp" +MANUFACTURER = "Mill" MAX_TEMP = 35 MIN_TEMP = 5 DOMAIN = "mill" diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 07eec93bb65..684be0479bd 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -3,5 +3,6 @@ "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", "requirements": ["millheater==0.3.4"], - "codeowners": ["@danielhiversen"] + "codeowners": ["@danielhiversen"], + "config_flow": true } diff --git a/homeassistant/components/mill/strings.json b/homeassistant/components/mill/strings.json new file mode 100644 index 00000000000..f9126c30f78 --- /dev/null +++ b/homeassistant/components/mill/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + } + } +} diff --git a/homeassistant/components/mill/translations/ca.json b/homeassistant/components/mill/translations/ca.json new file mode 100644 index 00000000000..0e877ca33cc --- /dev/null +++ b/homeassistant/components/mill/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "connection_error": "No s'ha pogut connectar" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/en.json b/homeassistant/components/mill/translations/en.json new file mode 100644 index 00000000000..231a9752d36 --- /dev/null +++ b/homeassistant/components/mill/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "connection_error": "Failed to connect" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/es.json b/homeassistant/components/mill/translations/es.json new file mode 100644 index 00000000000..fb0c69dfd07 --- /dev/null +++ b/homeassistant/components/mill/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "connection_error": "Fallo al conectarse" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/fi.json b/homeassistant/components/mill/translations/fi.json new file mode 100644 index 00000000000..61febe9dd9c --- /dev/null +++ b/homeassistant/components/mill/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/it.json b/homeassistant/components/mill/translations/it.json new file mode 100644 index 00000000000..7e3f909e048 --- /dev/null +++ b/homeassistant/components/mill/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_error": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/ko.json b/homeassistant/components/mill/translations/ko.json new file mode 100644 index 00000000000..c604519ca79 --- /dev/null +++ b/homeassistant/components/mill/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/lb.json b/homeassistant/components/mill/translations/lb.json new file mode 100644 index 00000000000..186e53931b6 --- /dev/null +++ b/homeassistant/components/mill/translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho registr\u00e9iert" + }, + "error": { + "connection_error": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json new file mode 100644 index 00000000000..3f41ac78c7d --- /dev/null +++ b/homeassistant/components/mill/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json new file mode 100644 index 00000000000..d84166e1cdf --- /dev/null +++ b/homeassistant/components/mill/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/zh-Hant.json b/homeassistant/components/mill/translations/zh-Hant.json new file mode 100644 index 00000000000..9682d5981df --- /dev/null +++ b/homeassistant/components/mill/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_error": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index cde2a414900..aadcba44e85 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -24,7 +24,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorDevice): +class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntity): """Representation of a Minecraft Server status binary sensor.""" def __init__(self, server: MinecraftServer) -> None: diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json index 0ca0b03134d..c0a0c78d5d9 100644 --- a/homeassistant/components/minecraft_server/strings.json +++ b/homeassistant/components/minecraft_server/strings.json @@ -4,7 +4,10 @@ "user": { "title": "Link your Minecraft Server", "description": "Set up your Minecraft Server instance to allow monitoring.", - "data": { "name": "Name", "host": "Host" } + "data": { + "name": "Name", + "host": "[%key:common::config_flow::data::host%]" + } } }, "error": { @@ -12,6 +15,8 @@ "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.", "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again." }, - "abort": { "already_configured": "Host is already configured." } + "abort": { + "already_configured": "Host is already configured." + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/ca.json b/homeassistant/components/minecraft_server/translations/ca.json index e34e2fb7fdb..6e5554216d9 100644 --- a/homeassistant/components/minecraft_server/translations/ca.json +++ b/homeassistant/components/minecraft_server/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3 amb el servidor. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que estas utilitzant la versi\u00f3 del servidor 1.7 o superior.", - "invalid_ip": "L\u2019adre\u00e7a IP \u00e9s inv\u00e0lida (no s\u2019ha pogut determinar l\u2019adre\u00e7a MAC). Corregeix-la i torna-ho a provar.", + "invalid_ip": "L'adre\u00e7a IP \u00e9s inv\u00e0lida (no s'ha pogut determinar l'adre\u00e7a MAC). Corregeix-la i torna-ho a provar.", "invalid_port": "El port ha d'estar compr\u00e8s entre 1024 i 65535. Corregeix-lo i torna-ho a provar." }, "step": { diff --git a/homeassistant/components/minecraft_server/translations/es-419.json b/homeassistant/components/minecraft_server/translations/es-419.json new file mode 100644 index 00000000000..46a3ee9a029 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "Error al conectar con el servidor. Verifique el host y el puerto e intente nuevamente. Tambi\u00e9n aseg\u00farese de que est\u00e9 ejecutando al menos Minecraft versi\u00f3n 1.7 en su servidor.", + "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor corr\u00edjalo e intente nuevamente.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Corr\u00edjalo e int\u00e9ntelo de nuevo." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre" + }, + "description": "Configure su instancia de Minecraft Server para permitir el monitoreo.", + "title": "Vincule su servidor Minecraft" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/fr.json b/homeassistant/components/minecraft_server/translations/fr.json index f2bb4dfa3de..44bd8230b4a 100644 --- a/homeassistant/components/minecraft_server/translations/fr.json +++ b/homeassistant/components/minecraft_server/translations/fr.json @@ -3,12 +3,18 @@ "abort": { "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." }, + "error": { + "cannot_connect": "\u00c9chec de connexion au serveur. Veuillez v\u00e9rifier l'h\u00f4te et le port et r\u00e9essayer. Assurez-vous \u00e9galement que vous ex\u00e9cutez au moins Minecraft version 1.7 sur votre serveur.", + "invalid_ip": "L'adresse IP n'est pas valide (l'adresse MAC n'a pas pu \u00eatre d\u00e9termin\u00e9e). Veuillez le corriger et r\u00e9essayer.", + "invalid_port": "Le port doit \u00eatre compris entre 1024 et 65535. Veuillez le corriger et r\u00e9essayer." + }, "step": { "user": { "data": { "host": "H\u00f4te", "name": "Nom" }, + "description": "Configurez votre instance Minecraft Server pour permettre la surveillance.", "title": "Reliez votre serveur Minecraft" } } diff --git a/homeassistant/components/minecraft_server/translations/ko.json b/homeassistant/components/minecraft_server/translations/ko.json index 9244acb2144..30605d72936 100644 --- a/homeassistant/components/minecraft_server/translations/ko.json +++ b/homeassistant/components/minecraft_server/translations/ko.json @@ -15,7 +15,7 @@ "name": "\uc774\ub984" }, "description": "\ubaa8\ub2c8\ud130\ub9c1\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d Minecraft \uc11c\ubc84 \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "Minecraft \uc11c\ubc84 \uc5f0\uacb0" + "title": "Minecraft \uc11c\ubc84 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/minecraft_server/translations/no.json b/homeassistant/components/minecraft_server/translations/no.json index bc3bb2955ad..4d2ecc6dbaa 100644 --- a/homeassistant/components/minecraft_server/translations/no.json +++ b/homeassistant/components/minecraft_server/translations/no.json @@ -14,7 +14,7 @@ "host": "Vert", "name": "Navn" }, - "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", + "description": "Sett opp Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", "title": "Link din Minecraft Server" } } diff --git a/homeassistant/components/minecraft_server/translations/pl.json b/homeassistant/components/minecraft_server/translations/pl.json index 77d83f37174..24749767b23 100644 --- a/homeassistant/components/minecraft_server/translations/pl.json +++ b/homeassistant/components/minecraft_server/translations/pl.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa" }, "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.", diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index c04a1af316d..ae8efc0c113 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,7 +1,7 @@ """Binary sensor platform for mobile_app.""" from functools import partial -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -57,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MobileAppBinarySensor(MobileAppEntity, BinarySensorDevice): +class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): """Representation of an mobile app binary sensor.""" @property diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index 697f676df78..84e613ca978 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Obre l\u2019aplicaci\u00f3 m\u00f2bil per configurar la integraci\u00f3 amb Home Assistant. Mira [la documentaci\u00f3]({apps_url}) per veure la llista d\u2019aplicacions compatibles." + "install_app": "Obre l'aplicaci\u00f3 m\u00f2bil per configurar la integraci\u00f3 amb Home Assistant. Mira [la documentaci\u00f3]({apps_url}) per veure la llista d'aplicacions compatibles." }, "step": { "confirm": { diff --git a/homeassistant/components/mobile_app/translations/es-419.json b/homeassistant/components/mobile_app/translations/es-419.json index cb666a7d72d..0ad6fb613f9 100644 --- a/homeassistant/components/mobile_app/translations/es-419.json +++ b/homeassistant/components/mobile_app/translations/es-419.json @@ -5,6 +5,7 @@ }, "step": { "confirm": { + "description": "\u00bfDesea configurar el componente de la aplicaci\u00f3n m\u00f3vil?", "title": "Aplicaci\u00f3n movil" } } diff --git a/homeassistant/components/mobile_app/translations/fi.json b/homeassistant/components/mobile_app/translations/fi.json new file mode 100644 index 00000000000..373ae986d8d --- /dev/null +++ b/homeassistant/components/mobile_app/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Mobiilisovellus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json index 860d455d582..131e007751c 100644 --- a/homeassistant/components/mobile_app/translations/no.json +++ b/homeassistant/components/mobile_app/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\u00c5pne mobilappen for \u00e5 konfigurere integrasjonen med hjemmevirksomheten. Se [docs]({apps_url}) for en liste over kompatible apper." + "install_app": "\u00c5pne mobilappen for \u00e5 sette opp integrasjonen med Home Assistant. Se [dokumentasjon]({apps_url}) for en liste over kompatible apper." }, "step": { "confirm": { diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 1f5cbc6bb95..a2264a65b16 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class MochadLight(Light): +class MochadLight(LightEntity): """Representation of a X10 dimmer over Mochad.""" def __init__(self, hass, ctrl, dev): diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 4ce8f676659..e7f1bee99f6 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -5,7 +5,7 @@ from pymochad import device from pymochad.exceptions import MochadException import voluptuous as vol -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class MochadSwitch(SwitchDevice): +class MochadSwitch(SwitchEntity): """Representation of a X10 switch over Mochad.""" def __init__(self, hass, ctrl, dev): diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 5f80813d108..ce66b7aecdb 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -9,7 +9,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, CONF_SLAVE from homeassistant.helpers import config_validation as cv @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class ModbusBinarySensor(BinarySensorDevice): +class ModbusBinarySensor(BinarySensorEntity): """Modbus binary sensor.""" def __init__(self, hub, name, slave, address, device_class, input_type): diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 5cfd9c36967..5498ed61738 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -7,7 +7,7 @@ from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, SUPPORT_TARGET_TEMPERATURE, @@ -115,7 +115,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ModbusThermostat(ClimateDevice): +class ModbusThermostat(ClimateEntity): """Representation of a Modbus Thermostat.""" def __init__( diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index e507717b22c..c12c50cdc07 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -25,6 +25,7 @@ DATA_TYPE_CUSTOM = "custom" DATA_TYPE_FLOAT = "float" DATA_TYPE_INT = "int" DATA_TYPE_UINT = "uint" +DATA_TYPE_STRING = "string" # call types CALL_TYPE_COIL = "coil" diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 8c475a114eb..8bf5f6f3115 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -34,6 +34,7 @@ from .const import ( DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, + DATA_TYPE_STRING, DATA_TYPE_UINT, DEFAULT_HUB, MODBUS_DOMAIN, @@ -69,7 +70,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_REGISTER): cv.positive_int, vol.Optional(CONF_COUNT, default=1): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( - [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] + [ + DATA_TYPE_INT, + DATA_TYPE_UINT, + DATA_TYPE_FLOAT, + DATA_TYPE_STRING, + DATA_TYPE_CUSTOM, + ] ), vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, @@ -92,13 +99,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus sensors.""" sensors = [] - data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}} - data_types[DATA_TYPE_UINT] = {1: "H", 2: "I", 4: "Q"} - data_types[DATA_TYPE_FLOAT] = {1: "e", 2: "f", 4: "d"} + data_types = { + DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}, + DATA_TYPE_UINT: {1: "H", 2: "I", 4: "Q"}, + DATA_TYPE_FLOAT: {1: "e", 2: "f", 4: "d"}, + } for register in config[CONF_REGISTERS]: structure = ">i" - if register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: + if register[CONF_DATA_TYPE] == DATA_TYPE_STRING: + structure = str(register[CONF_COUNT] * 2) + "s" + elif register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: try: structure = ( f">{data_types[register[CONF_DATA_TYPE]][register[CONF_COUNT]]}" @@ -142,6 +153,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): register[CONF_OFFSET], structure, register[CONF_PRECISION], + register[CONF_DATA_TYPE], register.get(CONF_DEVICE_CLASS), ) ) @@ -168,6 +180,7 @@ class ModbusRegisterSensor(RestoreEntity): offset, structure, precision, + data_type, device_class, ): """Initialize the modbus register sensor.""" @@ -183,6 +196,7 @@ class ModbusRegisterSensor(RestoreEntity): self._offset = offset self._precision = precision self._structure = structure + self._data_type = data_type self._device_class = device_class self._value = None self._available = True @@ -243,13 +257,16 @@ class ModbusRegisterSensor(RestoreEntity): registers.reverse() byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) - val = struct.unpack(self._structure, byte_string)[0] - val = self._scale * val + self._offset - if isinstance(val, int): - self._value = str(val) - if self._precision > 0: - self._value += "." + "0" * self._precision + if self._data_type != DATA_TYPE_STRING: + val = struct.unpack(self._structure, byte_string)[0] + val = self._scale * val + self._offset + if isinstance(val, int): + self._value = str(val) + if self._precision > 0: + self._value += "." + "0" * self._precision + else: + self._value = f"{val:.{self._precision}f}" else: - self._value = f"{val:.{self._precision}f}" + self._value = byte_string.decode() self._available = True diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 0d5a32c45e0..8037d926ef1 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -170,10 +170,8 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): self._available = False return - value = bool(result.bits[0]) self._available = True - - return value + return bool(result.bits[0]) def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" @@ -288,10 +286,9 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._available = False return - value = int(result.registers[0]) self._available = True - return value + return int(result.registers[0]) def _write_register(self, value): """Write holding register using the Modbus hub slave.""" diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 37593f6828e..9bceff1531c 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -10,7 +10,13 @@ from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import ( + CONF_NOT_FIRST_RUN, + DOMAIN, + FIRST_RUN, + MONOPRICE_OBJECT, + UNDO_UPDATE_LISTENER, +) PLATFORMS = ["media_player"] @@ -28,12 +34,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: monoprice = await hass.async_add_executor_job(get_monoprice, port) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = monoprice except SerialException: _LOGGER.error("Error connecting to Monoprice controller at %s", port) raise ConfigEntryNotReady - entry.add_update_listener(_update_listener) + # double negative to handle absence of value + first_run = not bool(entry.data.get(CONF_NOT_FIRST_RUN)) + + if first_run: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_NOT_FIRST_RUN: True} + ) + + undo_listener = entry.add_update_listener(_update_listener) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + MONOPRICE_OBJECT: monoprice, + UNDO_UPDATE_LISTENER: undo_listener, + FIRST_RUN: first_run, + } for component in PLATFORMS: hass.async_create_task( @@ -54,6 +73,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) + if unload_ok: + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index cbabc65a54b..6c6bc87bf28 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -99,7 +99,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @core.callback def _key_for_source(index, source, previous_sources): if str(index) in previous_sources: - key = vol.Optional(source, default=previous_sources[str(index)]) + key = vol.Optional( + source, description={"suggested_value": previous_sources[str(index)]} + ) else: key = vol.Optional(source) diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py index ea4667a77ff..576e4aa0e69 100644 --- a/homeassistant/components/monoprice/const.py +++ b/homeassistant/components/monoprice/const.py @@ -11,5 +11,11 @@ CONF_SOURCE_4 = "source_4" CONF_SOURCE_5 = "source_5" CONF_SOURCE_6 = "source_6" +CONF_NOT_FIRST_RUN = "not_first_run" + SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" + +FIRST_RUN = "first_run" +MONOPRICE_OBJECT = "monoprice_object" +UNDO_UPDATE_LISTENER = "update_update_listener" diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index c88673b2855..93cebc9d885 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -3,6 +3,6 @@ "name": "Monoprice 6-Zone Amplifier", "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": ["pymonoprice==0.3"], - "codeowners": ["@etsinko"], + "codeowners": ["@etsinko", "@OnFreund"], "config_flow": true } diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 9073ab224f1..4d6d337667e 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -4,7 +4,7 @@ import logging from serial import SerialException from homeassistant import core -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -16,7 +16,14 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv, entity_platform, service -from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT +from .const import ( + CONF_SOURCES, + DOMAIN, + FIRST_RUN, + MONOPRICE_OBJECT, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) _LOGGER = logging.getLogger(__name__) @@ -58,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Monoprice 6-zone amplifier platform.""" port = config_entry.data[CONF_PORT] - monoprice = hass.data[DOMAIN][config_entry.entry_id] + monoprice = hass.data[DOMAIN][config_entry.entry_id][MONOPRICE_OBJECT] sources = _get_sources(config_entry) @@ -71,7 +78,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): MonopriceZone(monoprice, sources, config_entry.entry_id, zone_id) ) - async_add_entities(entities, True) + # only call update before add if it's the first run so we can try to detect zones + first_run = hass.data[DOMAIN][config_entry.entry_id][FIRST_RUN] + async_add_entities(entities, first_run) platform = entity_platform.current_platform.get() @@ -107,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MonopriceZone(MediaPlayerDevice): +class MonopriceZone(MediaPlayerEntity): """Representation of a Monoprice amplifier zone.""" def __init__(self, monoprice, sources, namespace, zone_id): @@ -128,16 +137,19 @@ class MonopriceZone(MediaPlayerDevice): self._volume = None self._source = None self._mute = None + self._update_success = True def update(self): """Retrieve latest state.""" try: state = self._monoprice.zone_status(self._zone_id) except SerialException: + self._update_success = False _LOGGER.warning("Could not update zone %d", self._zone_id) return if not state: + self._update_success = False return self._state = STATE_ON if state.power else STATE_OFF @@ -152,7 +164,7 @@ class MonopriceZone(MediaPlayerDevice): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - return self._zone_id < 20 + return self._zone_id < 20 or self._update_success @property def device_info(self): diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json index 2b639587673..c25fb901d76 100644 --- a/homeassistant/components/monoprice/strings.json +++ b/homeassistant/components/monoprice/strings.json @@ -4,7 +4,7 @@ "user": { "title": "Connect to the device", "data": { - "port": "Serial port", + "port": "[%key:common::config_flow::data::port%]", "source_1": "Name of source #1", "source_2": "Name of source #2", "source_3": "Name of source #3", @@ -18,7 +18,9 @@ "cannot_connect": "Failed to connect, please try again", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } }, "options": { "step": { @@ -35,4 +37,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/en.json b/homeassistant/components/monoprice/translations/en.json index 3e62a066308..9e9f3a4d2cf 100644 --- a/homeassistant/components/monoprice/translations/en.json +++ b/homeassistant/components/monoprice/translations/en.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "Serial port", + "port": "Port", "source_1": "Name of source #1", "source_2": "Name of source #2", "source_3": "Name of source #3", diff --git a/homeassistant/components/monoprice/translations/es-419.json b/homeassistant/components/monoprice/translations/es-419.json new file mode 100644 index 00000000000..66c7d5417ce --- /dev/null +++ b/homeassistant/components/monoprice/translations/es-419.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "port": "Puerto serial", + "source_1": "Nombre de la fuente #1", + "source_2": "Nombre de la fuente #2", + "source_3": "Nombre de la fuente #3", + "source_4": "Nombre de la fuente #4", + "source_5": "Nombre de la fuente #5", + "source_6": "Nombre de la fuente #6" + }, + "title": "Conectarse al dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nombre de la fuente #1", + "source_2": "Nombre de la fuente #2", + "source_3": "Nombre de la fuente #3", + "source_4": "Nombre de la fuente #4", + "source_5": "Nombre de la fuente #5", + "source_6": "Nombre de la fuente #6" + }, + "title": "Configurar fuentes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/fr.json b/homeassistant/components/monoprice/translations/fr.json index f93fb82d444..63b3adc4a2a 100644 --- a/homeassistant/components/monoprice/translations/fr.json +++ b/homeassistant/components/monoprice/translations/fr.json @@ -17,7 +17,8 @@ "source_4": "Nom de la source #4", "source_5": "Nom de la source #5", "source_6": "Nom de la source #6" - } + }, + "title": "Se connecter \u00e0 l'appareil" } } }, diff --git a/homeassistant/components/monoprice/translations/ko.json b/homeassistant/components/monoprice/translations/ko.json index 4ba78dcfbbf..23e19173535 100644 --- a/homeassistant/components/monoprice/translations/ko.json +++ b/homeassistant/components/monoprice/translations/ko.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8", + "port": "\ud3ec\ud2b8", "source_1": "\uc785\ub825 \uc18c\uc2a4 1 \uc774\ub984", "source_2": "\uc785\ub825 \uc18c\uc2a4 2 \uc774\ub984", "source_3": "\uc785\ub825 \uc18c\uc2a4 3 \uc774\ub984", @@ -33,7 +33,7 @@ "source_5": "\uc785\ub825 \uc18c\uc2a4 5 \uc774\ub984", "source_6": "\uc785\ub825 \uc18c\uc2a4 6 \uc774\ub984" }, - "title": "\uc785\ub825 \uc18c\uc2a4 \uad6c\uc131" + "title": "\uc785\ub825 \uc18c\uc2a4 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/monoprice/translations/nl.json b/homeassistant/components/monoprice/translations/nl.json new file mode 100644 index 00000000000..74bc677dbe8 --- /dev/null +++ b/homeassistant/components/monoprice/translations/nl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "port": "Seri\u00eble poort", + "source_1": "Naam van bron #1", + "source_2": "Naam van bron #2", + "source_3": "Naam van bron #3", + "source_4": "Naam van bron #4", + "source_5": "Naam van bron #5", + "source_6": "Naam van bron #6" + }, + "title": "Verbinding maken met het apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Naam van bron #1", + "source_2": "Naam van bron #2", + "source_3": "Naam van bron #3", + "source_4": "Naam van bron #4", + "source_5": "Naam van bron #5", + "source_6": "Naam van bron #6" + }, + "title": "Configureer bronnen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/no.json b/homeassistant/components/monoprice/translations/no.json index bbbeed0c1fd..b95b9496951 100644 --- a/homeassistant/components/monoprice/translations/no.json +++ b/homeassistant/components/monoprice/translations/no.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "Serial port", + "port": "Seriell port", "source_1": "Navn p\u00e5 kilden #1", "source_2": "Navn p\u00e5 kilden #2", "source_3": "Navn p\u00e5 kilden #3", diff --git a/homeassistant/components/monoprice/translations/pl.json b/homeassistant/components/monoprice/translations/pl.json index 300c19e92b7..38dc9f9402f 100644 --- a/homeassistant/components/monoprice/translations/pl.json +++ b/homeassistant/components/monoprice/translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "port": "Port szeregowy", + "port": "[%key_id:common::config_flow::data::port%] szeregowy", "source_1": "Nazwa \u017ar\u00f3d\u0142a #1", "source_2": "Nazwa \u017ar\u00f3d\u0142a #2", "source_3": "Nazwa \u017ar\u00f3d\u0142a #3", diff --git a/homeassistant/components/monoprice/translations/ru.json b/homeassistant/components/monoprice/translations/ru.json index a16cf1ae38a..5b891db80a3 100644 --- a/homeassistant/components/monoprice/translations/ru.json +++ b/homeassistant/components/monoprice/translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", + "port": "\u041f\u043e\u0440\u0442", "source_1": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #1", "source_2": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #2", "source_3": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #3", diff --git a/homeassistant/components/monoprice/translations/sv.json b/homeassistant/components/monoprice/translations/sv.json new file mode 100644 index 00000000000..3531253b136 --- /dev/null +++ b/homeassistant/components/monoprice/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "title": "Anslut till enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index 9113e3c8d8a..ca1923d62d6 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "\u5e8f\u5217\u57e0", + "port": "\u901a\u8a0a\u57e0", "source_1": "\u4f86\u6e90 #1 \u540d\u7a31", "source_2": "\u4f86\u6e90 #2 \u540d\u7a31", "source_3": "\u4f86\u6e90 #3 \u540d\u7a31", diff --git a/homeassistant/components/moon/translations/sensor.es-419.json b/homeassistant/components/moon/translations/sensor.es-419.json index 107d3e46404..98242bed3ff 100644 --- a/homeassistant/components/moon/translations/sensor.es-419.json +++ b/homeassistant/components/moon/translations/sensor.es-419.json @@ -3,7 +3,12 @@ "moon__phase": { "first_quarter": "Cuarto creciente", "full_moon": "Luna llena", - "last_quarter": "Cuarto menguante" + "last_quarter": "Cuarto menguante", + "new_moon": "Luna nueva", + "waning_crescent": "Luna menguante", + "waning_gibbous": "Luna menguante gibosa", + "waxing_crescent": "Luna creciente", + "waxing_gibbous": "Luna creciente gibosa" } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.fi.json b/homeassistant/components/moon/translations/sensor.fi.json index 10f8bb9b8a6..f7788972044 100644 --- a/homeassistant/components/moon/translations/sensor.fi.json +++ b/homeassistant/components/moon/translations/sensor.fi.json @@ -1,12 +1,10 @@ { "state": { - "first_quarter": "Ensimm\u00e4inen nelj\u00e4nnes", - "full_moon": "T\u00e4ysikuu", - "last_quarter": "Viimeinen nelj\u00e4nnes", - "new_moon": "Uusikuu", - "waning_crescent": "V\u00e4henev\u00e4 sirppi", - "waning_gibbous": "V\u00e4henev\u00e4 kuperakuu", - "waxing_crescent": "Kasvava sirppi", - "waxing_gibbous": "Kasvava kuperakuu" + "moon__phase": { + "first_quarter": "Ensimm\u00e4inen nelj\u00e4nnes", + "full_moon": "T\u00e4ysikuu", + "last_quarter": "Viimeinen nelj\u00e4nnes", + "new_moon": "Uusikuu" + } } } \ No newline at end of file diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index b69fa651988..c09ab685597 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -5,7 +5,7 @@ import re import requests import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([MpcHcDevice(name, url)], True) -class MpcHcDevice(MediaPlayerDevice): +class MpcHcDevice(MediaPlayerEntity): """Representation of a MPC-HC server.""" def __init__(self, name, url): diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index bec61b10a9f..ba9b2f73d3c 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -6,7 +6,7 @@ import os import mpd import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([device], True) -class MpdDevice(MediaPlayerDevice): +class MpdDevice(MediaPlayerEntity): """Representation of a MPD server.""" # pylint: disable=no-member diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index e4262a7c548..2ec0ea0d203 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -69,6 +69,8 @@ ABBREVIATIONS = { "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", + "max_mirs": "max_mireds", + "min_mirs": "min_mireds", "max_temp": "max_temp", "min_temp": "min_temp", "mode_cmd_t": "mode_command_topic", @@ -159,6 +161,7 @@ ABBREVIATIONS = { "temp_lo_stat_t": "temperature_low_state_topic", "temp_stat_tpl": "temperature_state_template", "temp_stat_t": "temperature_state_topic", + "temp_unit": "temperature_unit", "tilt_clsd_val": "tilt_closed_value", "tilt_cmd_t": "tilt_command_topic", "tilt_inv_stat": "tilt_invert_state", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index ad39eecdcca..dae94b5a781 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -135,7 +135,7 @@ class MqttAlarm( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - alarm.AlarmControlPanel, + alarm.AlarmControlPanelEntity, ): """Representation of a MQTT alarm status.""" diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index c7595de0eeb..4e335dda959 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components import binary_sensor, mqtt from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import ( CONF_DEVICE, @@ -107,7 +107,7 @@ class MqttBinarySensor( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - BinarySensorDevice, + BinarySensorEntity, ): """Representation a binary sensor that is updated by MQTT.""" @@ -119,7 +119,11 @@ class MqttBinarySensor( self._sub_state = None self._expiration_trigger = None self._delay_listener = None - self._expired = None + expire_after = config.get(CONF_EXPIRE_AFTER) + if expire_after is not None and expire_after > 0: + self._expired = True + else: + self._expired = None device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 91e30c7b1b1..8cc6a3ffbdb 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, - ClimateDevice, + ClimateEntity, ) from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, @@ -273,7 +273,7 @@ class MqttClimate( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - ClimateDevice, + ClimateEntity, ): """Representation of an MQTT climate device.""" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index d5487bbe29f..4ed128f1ff3 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -16,7 +16,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, - CoverDevice, + CoverEntity, ) from homeassistant.const import ( CONF_DEVICE, @@ -205,7 +205,7 @@ class MqttCover( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - CoverDevice, + CoverEntity, ): """Representation of a cover that can be controlled using MQTT.""" diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 86850c61638..75e4b53a191 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -137,7 +137,13 @@ async def info_for_device(hass, device_id): { "topic": topic, "messages": [ - {"payload": msg.payload, "time": msg.timestamp, "topic": msg.topic} + { + "payload": msg.payload, + "qos": msg.qos, + "retain": msg.retain, + "time": msg.timestamp, + "topic": msg.topic, + } for msg in list(subscription["messages"]) ], } diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e6a7f827f1e..375318764d1 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -71,6 +71,8 @@ CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template" CONF_HS_COMMAND_TOPIC = "hs_command_topic" CONF_HS_STATE_TOPIC = "hs_state_topic" CONF_HS_VALUE_TEMPLATE = "hs_value_template" +CONF_MAX_MIREDS = "max_mireds" +CONF_MIN_MIREDS = "min_mireds" CONF_RGB_COMMAND_TEMPLATE = "rgb_command_template" CONF_RGB_COMMAND_TOPIC = "rgb_command_topic" CONF_RGB_STATE_TOPIC = "rgb_state_topic" @@ -86,7 +88,7 @@ CONF_WHITE_VALUE_TEMPLATE = "white_value_template" CONF_ON_COMMAND_TYPE = "on_command_type" DEFAULT_BRIGHTNESS_SCALE = 255 -DEFAULT_NAME = "MQTT Light" +DEFAULT_NAME = "MQTT LightEntity" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" @@ -116,6 +118,8 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_MAX_MIREDS): cv.positive_int, + vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In( VALUES_ON_COMMAND_TYPE @@ -160,7 +164,7 @@ class MqttLight( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, + LightEntity, RestoreEntity, ): """Representation of a MQTT light.""" @@ -564,6 +568,16 @@ class MqttLight( """Return the color temperature in mired.""" return self._color_temp + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return self._config.get(CONF_MIN_MIREDS, super().min_mireds) + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return self._config.get(CONF_MAX_MIREDS, super().max_mireds) + @property def white_value(self): """Return the white property.""" @@ -732,11 +746,13 @@ class MqttLight( ATTR_BRIGHTNESS in kwargs and self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None ): - percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 + brightness_normalized = kwargs[ATTR_BRIGHTNESS] / 255 brightness_scale = self._config[CONF_BRIGHTNESS_SCALE] device_brightness = min( - round(percent_bright * brightness_scale), brightness_scale + round(brightness_normalized * brightness_scale), brightness_scale ) + # Make sure the brightness is not rounded down to 0 + device_brightness = max(device_brightness, 1) mqtt.async_publish( self.hass, self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC], diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 924559e3e90..89ae0601fde 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -22,7 +22,7 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -81,6 +81,9 @@ CONF_FLASH_TIME_LONG = "flash_time_long" CONF_FLASH_TIME_SHORT = "flash_time_short" CONF_HS = "hs" +CONF_MAX_MIREDS = "max_mireds" +CONF_MIN_MIREDS = "min_mireds" + # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA_JSON = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( @@ -100,6 +103,8 @@ PLATFORM_SCHEMA_JSON = ( CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT ): cv.positive_int, vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, + vol.Optional(CONF_MAX_MIREDS): cv.positive_int, + vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All( @@ -131,7 +136,7 @@ class MqttLightJson( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, + LightEntity, RestoreEntity, ): """Representation of a MQTT JSON light.""" @@ -358,6 +363,16 @@ class MqttLightJson( """Return the color temperature in mired.""" return self._color_temp + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return self._config.get(CONF_MIN_MIREDS, super().min_mireds) + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return self._config.get(CONF_MAX_MIREDS, super().max_mireds) + @property def effect(self): """Return the current effect.""" @@ -428,9 +443,7 @@ class MqttLightJson( if self._brightness is not None: brightness = 255 else: - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 - ) + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100 ) @@ -461,11 +474,14 @@ class MqttLightJson( message["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and self._brightness is not None: - message["brightness"] = int( - kwargs[ATTR_BRIGHTNESS] - / float(DEFAULT_BRIGHTNESS_SCALE) - * self._config[CONF_BRIGHTNESS_SCALE] + brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_SCALE + brightness_scale = self._config[CONF_BRIGHTNESS_SCALE] + device_brightness = min( + round(brightness_normalized * brightness_scale), brightness_scale ) + # Make sure the brightness is not rounded down to 0 + device_brightness = max(device_brightness, 1) + message["brightness"] = device_brightness if self._optimistic: self._brightness = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 25de6862bdb..a9f18e7039b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -63,6 +63,8 @@ CONF_COMMAND_ON_TEMPLATE = "command_on_template" CONF_EFFECT_LIST = "effect_list" CONF_EFFECT_TEMPLATE = "effect_template" CONF_GREEN_TEMPLATE = "green_template" +CONF_MAX_MIREDS = "max_mireds" +CONF_MIN_MIREDS = "min_mireds" CONF_RED_TEMPLATE = "red_template" CONF_STATE_TEMPLATE = "state_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" @@ -79,6 +81,8 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template, + vol.Optional(CONF_MAX_MIREDS): cv.positive_int, + vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RED_TEMPLATE): cv.template, @@ -105,7 +109,7 @@ class MqttTemplate( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, + LightEntity, RestoreEntity, ): """Representation of a MQTT Template light.""" @@ -337,6 +341,16 @@ class MqttTemplate( """Return the color temperature in mired.""" return self._color_temp + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return self._config.get(CONF_MIN_MIREDS, super().min_mireds) + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return self._config.get(CONF_MAX_MIREDS, super().max_mireds) + @property def hs_color(self): """Return the hs color value [int, int].""" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 378f1b8fbcb..34905fe8aa4 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import lock, mqtt -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -108,7 +108,7 @@ class MqttLock( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - LockDevice, + LockEntity, ): """Representation of a lock that can be toggled using MQTT.""" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 08a1500975a..305f3a206a7 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -5,22 +5,26 @@ "description": "Please enter the connection information of your MQTT broker.", "data": { "broker": "Broker", - "port": "Port", - "username": "Username", - "password": "Password", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", "discovery": "Enable discovery" } }, "hassio_confirm": { "title": "MQTT Broker via Hass.io add-on", "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?", - "data": { "discovery": "Enable discovery" } + "data": { + "discovery": "Enable discovery" + } } }, "abort": { "single_instance_allowed": "Only a single configuration of MQTT is allowed." }, - "error": { "cannot_connect": "Unable to connect to the broker." } + "error": { + "cannot_connect": "Unable to connect to the broker." + } }, "device_automation": { "trigger_type": { @@ -44,4 +48,4 @@ "button_6": "Sixth button" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index bc1f4a038b4..44d028829d0 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import mqtt, switch -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( CONF_DEVICE, CONF_ICON, @@ -104,7 +104,7 @@ class MqttSwitch( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - SwitchDevice, + SwitchEntity, RestoreEntity, ): """Representation of a switch that can be toggled using MQTT.""" diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index af0bd912bc7..afb55aca1ce 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -26,5 +26,27 @@ "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" pulsado 2 veces", + "button_long_press": "\"{subtype}\" pulsado continuamente", + "button_long_release": "Se solt\u00f3 \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "button_quadruple_press": "\"{subtype}\" pulsado 4 veces", + "button_quintuple_press": "\"{subtype}\" pulsado 5 veces", + "button_short_press": "\"{subtype}\" presionado", + "button_short_release": "\"{subtype}\" soltado", + "button_triple_press": "\"{subtype}\" pulsado 3 veces" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 131e0855ef9..5ba2211f912 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -22,7 +22,7 @@ "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfDesea configurar Home Assistant para conectarse al agente MQTT provisto por el complemento hass.io {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectar con el broker de MQTT proporcionado por el complemento Hass.io {addon}?", "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" } } diff --git a/homeassistant/components/mqtt/translations/fi.json b/homeassistant/components/mqtt/translations/fi.json new file mode 100644 index 00000000000..3659e489d8a --- /dev/null +++ b/homeassistant/components/mqtt/translations/fi.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "broker": "V\u00e4litt\u00e4j\u00e4", + "discovery": "Ota etsint\u00e4 k\u00e4ytt\u00f6\u00f6n", + "password": "Salasana", + "port": "Portti", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Ota etsint\u00e4 k\u00e4ytt\u00f6\u00f6n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 74eec7a1f11..f97807b015f 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -39,7 +39,14 @@ "turn_on": "Allumer" }, "trigger_type": { - "button_short_press": "\" {subtype} \" press\u00e9" + "button_double_press": "\"{subtype}\" double-cliqu\u00e9", + "button_long_press": "\" {subtype} \" press\u00e9 en continu", + "button_long_release": "\"{subtype}\" relach\u00e9 apr\u00e8s un appui long", + "button_quadruple_press": "\"{subtype}\" quadruple cliqu\u00e9", + "button_quintuple_press": "\"{subtype}\" quintuple cliqu\u00e9", + "button_short_press": "\" {subtype} \" press\u00e9", + "button_short_release": "\"{subtype}\" relach\u00e9", + "button_triple_press": "\"{subtype}\" triple-cliqu\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 56684b32535..f7c099c2cb6 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -26,5 +26,16 @@ "title": "MQTTT Broker via Hass.io add-on" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", + "turn_off": "Uitschakelen" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 3bfceb324a3..456b757cb88 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -12,11 +12,11 @@ "broker": "Megler", "discovery": "Aktiver oppdagelse", "password": "Passord", - "port": "", + "port": "Port", "username": "Brukernavn" }, - "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", - "title": "MQTT" + "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler.", + "title": "" }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 73a29caced6..4d5044e1fb0 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -11,9 +11,9 @@ "data": { "broker": "Po\u015brednik", "discovery": "W\u0142\u0105cz wykrywanie", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", "title": "MQTT" diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 58855eeb62b..9415beedbf7 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -26,5 +26,17 @@ "title": "MQTT Broker via Hass.io till\u00e4gg" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "button_5": "Femte knappen", + "button_6": "Sj\u00e4tte knappen", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 9b24b09b10e..0bf4064bf52 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -11,7 +11,7 @@ "data": { "broker": "Broker", "discovery": "\u958b\u555f\u641c\u5c0b", - "password": "\u4f7f\u7528\u8005\u5bc6\u78bc", + "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 85851bcf696..c4259272bb3 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -25,7 +25,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumDevice, + VacuumEntity, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME from homeassistant.core import callback @@ -174,7 +174,7 @@ class MqttVacuum( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - VacuumDevice, + VacuumEntity, ): """Representation of a MQTT-controlled legacy vacuum.""" diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index a59beae1d34..9049df45110 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -34,7 +34,7 @@ from homeassistant.components.vacuum import ( SUPPORT_START, SUPPORT_STATUS, SUPPORT_STOP, - StateVacuumDevice, + StateVacuumEntity, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME from homeassistant.core import callback @@ -162,7 +162,7 @@ class MqttStateVacuum( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - StateVacuumDevice, + StateVacuumEntity, ): """Representation of a MQTT-controlled state vacuum.""" diff --git a/homeassistant/components/mychevy/binary_sensor.py b/homeassistant/components/mychevy/binary_sensor.py index 702f3146f8e..b009fe3b3f4 100644 --- a/homeassistant/components/mychevy/binary_sensor.py +++ b/homeassistant/components/mychevy/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.core import callback from homeassistant.util import slugify @@ -29,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class EVBinarySensor(BinarySensorDevice): +class EVBinarySensor(BinarySensorEntity): """Base EVSensor class. The only real difference between sensors is which units and what diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index a54b8e50ece..aa6c886286c 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -1,23 +1,22 @@ """Support for MyQ gateways.""" import logging -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_CONNECTIVITY, - BinarySensorDevice, -) - -from .const import ( - DOMAIN, +from pymyq.const import ( + DEVICE_FAMILY as MYQ_DEVICE_FAMILY, + DEVICE_FAMILY_GATEWAY as MYQ_DEVICE_FAMILY_GATEWAY, + DEVICE_STATE as MYQ_DEVICE_STATE, + DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, KNOWN_MODELS, MANUFACTURER, - MYQ_COORDINATOR, - MYQ_DEVICE_FAMILY, - MYQ_DEVICE_FAMILY_GATEWAY, - MYQ_DEVICE_STATE, - MYQ_DEVICE_STATE_ONLINE, - MYQ_GATEWAY, ) +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) + +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY + _LOGGER = logging.getLogger(__name__) @@ -31,12 +30,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device in myq.devices.values(): if device.device_json[MYQ_DEVICE_FAMILY] == MYQ_DEVICE_FAMILY_GATEWAY: - entities.append(MyQBinarySensorDevice(coordinator, device)) + entities.append(MyQBinarySensorEntity(coordinator, device)) async_add_entities(entities, True) -class MyQBinarySensorDevice(BinarySensorDevice): +class MyQBinarySensorEntity(BinarySensorEntity): """Representation of a MyQ gateway.""" def __init__(self, coordinator, device): diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 07d57921e35..4fd267f1b21 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -75,6 +75,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # they already have one configured as they can always # add a new one via "+" return self.async_abort(reason="already_configured") + properties = { + key.lower(): value for (key, value) in homekit_info["properties"].items() + } + await self.async_set_unique_id(properties["id"]) return await self.async_step_user() async def async_step_import(self, user_input): diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 352c19ebd24..9251bce7447 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -12,16 +12,6 @@ DOMAIN = "myq" PLATFORMS = ["cover", "binary_sensor"] -MYQ_DEVICE_TYPE = "device_type" -MYQ_DEVICE_TYPE_GATE = "gate" - -MYQ_DEVICE_FAMILY = "device_family" -MYQ_DEVICE_FAMILY_GATEWAY = "gateway" - -MYQ_DEVICE_STATE = "state" -MYQ_DEVICE_STATE_ONLINE = "online" - - MYQ_TO_HASS = { MYQ_STATE_CLOSED: STATE_CLOSED, MYQ_STATE_CLOSING: STATE_CLOSING, @@ -43,36 +33,3 @@ TRANSITION_START_DURATION = 7 # Estimated time it takes myq to complete a transition # from one state to another TRANSITION_COMPLETE_DURATION = 37 - -MANUFACTURER = "The Chamberlain Group Inc." - -KNOWN_MODELS = { - "00": "Chamberlain Ethernet Gateway", - "01": "LiftMaster Ethernet Gateway", - "02": "Craftsman Ethernet Gateway", - "03": "Chamberlain Wi-Fi hub", - "04": "LiftMaster Wi-Fi hub", - "05": "Craftsman Wi-Fi hub", - "08": "LiftMaster Wi-Fi GDO DC w/Battery Backup", - "09": "Chamberlain Wi-Fi GDO DC w/Battery Backup", - "10": "Craftsman Wi-Fi GDO DC 3/4HP", - "11": "MyQ Replacement Logic Board Wi-Fi GDO DC 3/4HP", - "12": "Chamberlain Wi-Fi GDO DC 1.25HP", - "13": "LiftMaster Wi-Fi GDO DC 1.25HP", - "14": "Craftsman Wi-Fi GDO DC 1.25HP", - "15": "MyQ Replacement Logic Board Wi-Fi GDO DC 1.25HP", - "0A": "Chamberlain Wi-Fi GDO or Gate Operator AC", - "0B": "LiftMaster Wi-Fi GDO or Gate Operator AC", - "0C": "Craftsman Wi-Fi GDO or Gate Operator AC", - "0D": "MyQ Replacement Logic Board Wi-Fi GDO or Gate Operator AC", - "0E": "Chamberlain Wi-Fi GDO DC 3/4HP", - "0F": "LiftMaster Wi-Fi GDO DC 3/4HP", - "20": "Chamberlain MyQ Home Bridge", - "21": "LiftMaster MyQ Home Bridge", - "23": "Chamberlain Smart Garage Hub", - "24": "LiftMaster Smart Garage Hub", - "27": "LiftMaster Wi-Fi Wall Mount opener", - "28": "LiftMaster Commercial Wi-Fi Wall Mount operator", - "80": "EU LiftMaster Ethernet Gateway", - "81": "EU Chamberlain Ethernet Gateway", -} diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 04eb49c00a9..9a44234e747 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -2,6 +2,14 @@ import logging import time +from pymyq.const import ( + DEVICE_STATE as MYQ_DEVICE_STATE, + DEVICE_STATE_ONLINE as MYQ_DEVICE_STATE_ONLINE, + DEVICE_TYPE as MYQ_DEVICE_TYPE, + DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE, + KNOWN_MODELS, + MANUFACTURER, +) import voluptuous as vol from homeassistant.components.cover import ( @@ -10,7 +18,7 @@ from homeassistant.components.cover import ( PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, - CoverDevice, + CoverEntity, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( @@ -27,13 +35,7 @@ from homeassistant.helpers.event import async_call_later from .const import ( DOMAIN, - KNOWN_MODELS, - MANUFACTURER, MYQ_COORDINATOR, - MYQ_DEVICE_STATE, - MYQ_DEVICE_STATE_ONLINE, - MYQ_DEVICE_TYPE, - MYQ_DEVICE_TYPE_GATE, MYQ_GATEWAY, MYQ_TO_HASS, TRANSITION_COMPLETE_DURATION, @@ -80,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MyQDevice(CoverDevice): +class MyQDevice(CoverEntity): """Representation of a MyQ cover.""" def __init__(self, coordinator, device): diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json index a7300b16598..566b6a0fdac 100644 --- a/homeassistant/components/myq/strings.json +++ b/homeassistant/components/myq/strings.json @@ -3,7 +3,10 @@ "step": { "user": { "title": "Connect to the MyQ Gateway", - "data": { "username": "Username", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -11,6 +14,8 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "abort": { "already_configured": "MyQ is already configured" } + "abort": { + "already_configured": "MyQ is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/es-419.json b/homeassistant/components/myq/translations/es-419.json new file mode 100644 index 00000000000..3d2f7a6ea71 --- /dev/null +++ b/homeassistant/components/myq/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a MyQ Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json new file mode 100644 index 00000000000..fd6310cce6a --- /dev/null +++ b/homeassistant/components/myq/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Verbinding maken met de MyQ-gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/pl.json b/homeassistant/components/myq/translations/pl.json index 74220a6518b..30a4221134e 100644 --- a/homeassistant/components/myq/translations/pl.json +++ b/homeassistant/components/myq/translations/pl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Po\u0142\u0105czenie z bramk\u0105 MyQ" } diff --git a/homeassistant/components/myq/translations/sv.json b/homeassistant/components/myq/translations/sv.json new file mode 100644 index 00000000000..1243ca600f0 --- /dev/null +++ b/homeassistant/components/myq/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 0b94e764937..0bab6ea6eea 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -3,7 +3,7 @@ from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, DOMAIN, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import STATE_ON @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorDevice): +class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity): """Representation of a MySensors Binary Sensor child node.""" @property diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index b00a0d0d9d5..c318ccf7ec6 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,6 +1,6 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" from homeassistant.components import mysensors -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -43,7 +43,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): +class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): """Representation of a MySensors HVAC.""" @property diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index b60cf9457a9..f2ede69793f 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,6 +1,6 @@ """Support for MySensors covers.""" from homeassistant.components import mysensors -from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice +from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity from homeassistant.const import STATE_OFF, STATE_ON @@ -15,7 +15,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): +class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): """Representation of the value of a MySensors Cover child node.""" @property diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 1585de4b462..ffbcba6f032 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback @@ -34,7 +34,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MySensorsLight(mysensors.device.MySensorsEntity, Light): +class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): """Representation of a MySensors Light child node.""" def __init__(self, *args): diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 16bb1ee6deb..0da8bfe7030 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -2,7 +2,7 @@ import voluptuous as vol from homeassistant.components import mysensors -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv @@ -72,7 +72,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): +class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): """Representation of the value of a MySensors Switch child node.""" @property diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index c584440874a..87c1a3a2665 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the myStrom buttons.""" import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY from homeassistant.core import callback @@ -59,7 +59,7 @@ class MyStromView(HomeAssistantView): self.buttons[entity_id].async_on_update(new_state) -class MyStromBinarySensor(BinarySensorDevice): +class MyStromBinarySensor(BinarySensorEntity): """Representation of a myStrom button.""" def __init__(self, button_id): diff --git a/homeassistant/components/mystrom/const.py b/homeassistant/components/mystrom/const.py new file mode 100644 index 00000000000..87697acbe96 --- /dev/null +++ b/homeassistant/components/mystrom/const.py @@ -0,0 +1,2 @@ +"""Constants for the myStrom integration.""" +DOMAIN = "mystrom" diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index 56fe369144b..2762792e133 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -14,9 +14,10 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_EFFECT, SUPPORT_FLASH, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,28 +40,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the myStrom Light platform.""" - +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the myStrom light integration.""" host = config.get(CONF_HOST) mac = config.get(CONF_MAC) name = config.get(CONF_NAME) bulb = MyStromBulb(host, mac) try: - if bulb.get_status()["type"] != "rgblamp": + await bulb.get_state() + if bulb.bulb_type != "rgblamp": _LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac) return except MyStromConnectionError: - _LOGGER.warning("No route to device: %s", host) + _LOGGER.warning("No route to myStrom bulb: %s", host) + raise PlatformNotReady() - add_entities([MyStromLight(bulb, name)], True) + async_add_entities([MyStromLight(bulb, name, mac)], True) -class MyStromLight(Light): - """Representation of the myStrom WiFi Bulb.""" +class MyStromLight(LightEntity): + """Representation of the myStrom WiFi bulb.""" - def __init__(self, bulb, name): + def __init__(self, bulb, name, mac): """Initialize the light.""" self._bulb = bulb self._name = name @@ -69,12 +71,18 @@ class MyStromLight(Light): self._brightness = 0 self._color_h = 0 self._color_s = 0 + self._mac = mac @property def name(self): """Return the display name of this light.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._mac + @property def supported_features(self): """Flag supported features.""" @@ -103,11 +111,10 @@ class MyStromLight(Light): @property def is_on(self): """Return true if light is on.""" - return self._state["on"] if self._state is not None else None + return self._state - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn on the light.""" - brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) @@ -121,33 +128,32 @@ class MyStromLight(Light): try: if not self.is_on: - self._bulb.set_on() + await self._bulb.set_on() if brightness is not None: - self._bulb.set_color_hsv( + await self._bulb.set_color_hsv( int(color_h), int(color_s), round(brightness * 100 / 255) ) if effect == EFFECT_SUNRISE: - self._bulb.set_sunrise(30) + await self._bulb.set_sunrise(30) if effect == EFFECT_RAINBOW: - self._bulb.set_rainbow(30) + await self._bulb.set_rainbow(30) except MyStromConnectionError: - _LOGGER.warning("No route to device") + _LOGGER.warning("No route to myStrom bulb") - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off the bulb.""" - try: - self._bulb.set_off() + await self._bulb.set_off() except MyStromConnectionError: _LOGGER.warning("myStrom bulb not online") - def update(self): + async def async_update(self): """Fetch new state data for this light.""" - try: - self._state = self._bulb.get_status() + await self._bulb.get_state() + self._state = self._bulb.state - colors = self._bulb.get_color()["color"] + colors = self._bulb.color try: color_h, color_s, color_v = colors.split(";") except ValueError: @@ -160,5 +166,5 @@ class MyStromLight(Light): self._available = True except MyStromConnectionError: - _LOGGER.warning("No route to device") + _LOGGER.warning("No route to myStrom bulb") self._available = False diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 7d74fca92a2..71a719be92a 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -2,7 +2,7 @@ "domain": "mystrom", "name": "myStrom", "documentation": "https://www.home-assistant.io/integrations/mystrom", - "requirements": ["python-mystrom==0.5.0"], + "requirements": ["python-mystrom==1.1.2"], "dependencies": ["http"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 5bfd1e45b81..ab1207658ab 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -1,11 +1,11 @@ -"""Support for myStrom switches.""" +"""Support for myStrom switches/plugs.""" import logging from pymystrom.exceptions import MyStromConnectionError -from pymystrom.switch import MyStromPlug +from pymystrom.switch import MyStromSwitch as _MyStromSwitch 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_HOST, CONF_NAME from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -22,30 +22,30 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Find and return myStrom switch.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the myStrom switch/plug integration.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) try: - MyStromPlug(host).get_status() + plug = _MyStromSwitch(host) + await plug.get_state() except MyStromConnectionError: - _LOGGER.error("No route to device: %s", host) + _LOGGER.error("No route to myStrom plug: %s", host) raise PlatformNotReady() - add_entities([MyStromSwitch(name, host)]) + async_add_entities([MyStromSwitch(plug, name)]) -class MyStromSwitch(SwitchDevice): - """Representation of a myStrom switch.""" +class MyStromSwitch(SwitchEntity): + """Representation of a myStrom switch/plug.""" - def __init__(self, name, resource): - """Initialize the myStrom switch.""" + def __init__(self, plug, name): + """Initialize the myStrom switch/plug.""" self._name = name - self._resource = resource - self.data = {} - self.plug = MyStromPlug(self._resource) + self.plug = plug self._available = True + self.relay = None @property def name(self): @@ -55,38 +55,43 @@ class MyStromSwitch(SwitchDevice): @property def is_on(self): """Return true if switch is on.""" - return bool(self.data["relay"]) + return bool(self.relay) + + @property + def unique_id(self): + """Return a unique ID.""" + return self.plug._mac # pylint: disable=protected-access @property def current_power_w(self): """Return the current power consumption in W.""" - return round(self.data["power"], 2) + return self.plug.consumption @property def available(self): """Could the device be accessed during the last update call.""" return self._available - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" try: - self.plug.set_relay_on() + await self.plug.turn_on() except MyStromConnectionError: - _LOGGER.error("No route to device: %s", self._resource) + _LOGGER.error("No route to myStrom plug") - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" try: - self.plug.set_relay_off() + await self.plug.turn_off() except MyStromConnectionError: - _LOGGER.error("No route to device: %s", self._resource) + _LOGGER.error("No route to myStrom plug") - def update(self): + async def async_update(self): """Get the latest data from the device and update the data.""" try: - self.data = self.plug.get_status() + await self.plug.get_state() + self.relay = self.plug.relay self._available = True except MyStromConnectionError: - self.data = {"power": 0, "relay": False} self._available = False - _LOGGER.error("No route to device: %s", self._resource) + _LOGGER.error("No route to myStrom plug") diff --git a/homeassistant/components/n26/switch.py b/homeassistant/components/n26/switch.py index 6ec111720f3..155c9c0091b 100644 --- a/homeassistant/components/n26/switch.py +++ b/homeassistant/components/n26/switch.py @@ -1,7 +1,7 @@ """Support for N26 switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DEFAULT_SCAN_INTERVAL, DOMAIN from .const import CARD_STATE_ACTIVE, CARD_STATE_BLOCKED, DATA @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switch_entities) -class N26CardSwitch(SwitchDevice): +class N26CardSwitch(SwitchEntity): """Representation of a N26 card block/unblock switch.""" def __init__(self, api_data, card: dict): diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 0c29aac427f..2d9afbb7541 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -4,7 +4,7 @@ import logging from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -105,7 +105,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class NAD(MediaPlayerDevice): +class NAD(MediaPlayerEntity): """Representation of a NAD Receiver.""" def __init__(self, name, nad_receiver, min_volume, max_volume, source_dict): @@ -197,7 +197,10 @@ class NAD(MediaPlayerDevice): else: self._mute = True - self._volume = self.calc_volume(self._nad_receiver.main_volume("?")) + volume = self._nad_receiver.main_volume("?") + # Some receivers cannot report the volume, e.g. C 356BEE, + # instead they only support stepping the volume up or down + self._volume = self.calc_volume(volume) if volume is not None else None self._source = self._source_dict.get(self._nad_receiver.main_source("?")) def calc_volume(self, decibel): @@ -221,7 +224,7 @@ class NAD(MediaPlayerDevice): ) -class NADtcp(MediaPlayerDevice): +class NADtcp(MediaPlayerEntity): """Representation of a NAD Digital amplifier.""" def __init__(self, name, nad_device, min_volume, max_volume, volume_step): diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 612e1b6ead9..5073a421e49 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv @@ -103,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([NanoleafLight(nanoleaf_light, name)], True) -class NanoleafLight(Light): +class NanoleafLight(LightEntity): """Representation of a Nanoleaf Light.""" def __init__(self, light, name): diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index f104a37a93a..024a30a8e2d 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -4,8 +4,8 @@ "user": { "title": "Neato Account Info", "data": { - "username": "Username", - "password": "Password", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", "vendor": "Vendor" }, "description": "See [Neato documentation]({docs_url})." @@ -15,10 +15,12 @@ "invalid_credentials": "Invalid credentials", "unexpected_error": "Unexpected error" }, - "create_entry": { "default": "See [Neato documentation]({docs_url})." }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, "abort": { "already_configured": "Already configured", "invalid_credentials": "Invalid credentials" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es-419.json b/homeassistant/components/neato/translations/es-419.json new file mode 100644 index 00000000000..a6a684597f6 --- /dev/null +++ b/homeassistant/components/neato/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Consulte [Documentaci\u00f3n de Neato] ({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario", + "vendor": "Vendedor" + }, + "description": "Consulte [Documentaci\u00f3n de Neato] ({docs_url}).", + "title": "Informaci\u00f3n de cuenta de Neato" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index 63ce9b7bec0..de8932cd570 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "Allerede konfigurert", - "invalid_credentials": "Ugyldig brukerinformasjon" + "invalid_credentials": "Ugyldig legitimasjon" }, "create_entry": { "default": "Se [Neato dokumentasjon]({docs_url})." }, "error": { - "invalid_credentials": "Ugyldig brukerinformasjon", + "invalid_credentials": "Ugyldig legitimasjon", "unexpected_error": "Uventet feil" }, "step": { diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index a040c880e33..c7b37fa083d 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -9,13 +9,13 @@ }, "error": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "unexpected_error": "Niespodziewany b\u0142\u0105d" + "unexpected_error": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika", + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]", "vendor": "Dostawca" }, "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 391fcecf373..a67b48169c4 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -22,7 +22,7 @@ from homeassistant.components.vacuum import ( SUPPORT_START, SUPPORT_STATE, SUPPORT_STOP, - StateVacuumDevice, + StateVacuumEntity, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE import homeassistant.helpers.config_validation as cv @@ -126,7 +126,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class NeatoConnectedVacuum(StateVacuumDevice): +class NeatoConnectedVacuum(StateVacuumEntity): """Representation of a Neato Connected Vacuum.""" def __init__(self, neato, robot, mapdata, persistent_maps): diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 10291802fed..01372e744fb 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -2,6 +2,6 @@ "domain": "nederlandse_spoorwegen", "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", - "requirements": ["nsapi==3.0.3"], + "requirements": ["nsapi==3.0.4"], "codeowners": ["@YarmoM"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 2db46b02db4..39c05ff7cbf 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -148,7 +148,6 @@ class NSDepartureSensor(Entity): "arrival_platform_planned": self._trips[0].arrival_platform_planned, "arrival_platform_actual": self._trips[0].arrival_platform_actual, "next": None, - "punctuality": None, "status": self._trips[0].status.lower(), "transfers": self._trips[0].nr_transfers, "route": route, @@ -197,10 +196,6 @@ class NSDepartureSensor(Entity): ): attributes["arrival_delay"] = True - # Punctuality attributes - if self._trips[0].punctuality is not None: - attributes["punctuality"] = self._trips[0].punctuality - # Next attributes if len(self._trips) > 1: if self._trips[1].departure_time_actual is not None: diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 19f8e7aa14c..dc761d61461 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -5,7 +5,7 @@ import logging from pynello.private import Nello import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice +from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -27,7 +27,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([NelloLock(lock) for lock in nello.locations], True) -class NelloLock(LockDevice): +class NelloLock(LockEntity): """Representation of a Nello lock.""" def __init__(self, nello_lock): diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 8181e54640d..d92e944c10f 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -34,7 +34,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([device]) -class NessAlarmPanel(alarm.AlarmControlPanel): +class NessAlarmPanel(alarm.AlarmControlPanelEntity): """Representation of a Ness alarm panel.""" def __init__(self, client, name): diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index c719febdb58..d7e7851792e 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Ness D8X/D16X zone states - represented as binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class NessZoneBinarySensor(BinarySensorDevice): +class NessZoneBinarySensor(BinarySensorEntity): """Representation of an Ness alarm zone as a binary sensor.""" def __init__(self, zone_id, name, zone_type): diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 34dc7b06ade..dd52e1d665f 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -2,7 +2,7 @@ from itertools import chain import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice @@ -114,7 +114,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(await hass.async_add_job(get_binary_sensors), True) -class NestBinarySensor(NestSensorDevice, BinarySensorDevice): +class NestBinarySensor(NestSensorDevice, BinarySensorEntity): """Represents a Nest binary sensor.""" @property diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 92442479091..9a84010a3bc 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -4,7 +4,7 @@ import logging from nest.nest import APIError import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -88,7 +88,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(all_devices, True) -class NestThermostat(ClimateDevice): +class NestThermostat(ClimateEntity): """Representation of a Nest thermostat.""" def __init__(self, structure, device, temp_unit): diff --git a/homeassistant/components/nest/translations/es-419.json b/homeassistant/components/nest/translations/es-419.json index d816813855c..f7e19809f6b 100644 --- a/homeassistant/components/nest/translations/es-419.json +++ b/homeassistant/components/nest/translations/es-419.json @@ -3,10 +3,13 @@ "abort": { "already_setup": "Solo puedes configurar una sola cuenta Nest.", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { + "internal_error": "C\u00f3digo de validaci\u00f3n de error interno", "invalid_code": "Codigo invalido", + "timeout": "Tiempo de espera agotado para validar el c\u00f3digo", "unknown": "Error desconocido al validar el c\u00f3digo" }, "step": { diff --git a/homeassistant/components/nest/translations/fi.json b/homeassistant/components/nest/translations/fi.json new file mode 100644 index 00000000000..561b03f9286 --- /dev/null +++ b/homeassistant/components/nest/translations/fi.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "Voit m\u00e4\u00e4ritt\u00e4\u00e4 vain yhden Nest-tilin.", + "authorize_url_fail": "Tuntematon virhe luotaessa valtuutuksen URL-osoitetta." + }, + "error": { + "invalid_code": "Virheellinen koodi" + }, + "step": { + "init": { + "data": { + "flow_impl": "Tarjoaja" + } + }, + "link": { + "data": { + "code": "Pin-koodi" + }, + "title": "Linkit\u00e4 Nest-tili" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index 08c2f54ba7a..ee56fc4f11e 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -25,7 +25,7 @@ "code": "PIN \ucf54\ub4dc" }, "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 PIN \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", - "title": "Nest \uacc4\uc815 \uc5f0\uacb0" + "title": "Nest \uacc4\uc815 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 0adab5dcdcd..70b6ca569a7 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere en Nest konto.", - "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." + "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", + "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan godkjenne den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Intern feil ved validering av kode", @@ -17,14 +17,14 @@ "data": { "flow_impl": "Tilbyder" }, - "description": "Velg via hvilken autentiseringstilbyder du vil godkjenne med Nest.", - "title": "Autentiseringstilbyder" + "description": "Velg fra hvilken godkjenningsleverand\u00f8r du vil godkjenne med Nest.", + "title": "Godkjenningsleverand\u00f8r" }, "link": { "data": { "code": "PIN kode" }, - "description": "For \u00e5 koble din Nest-konto, [autoriser kontoen din]({url}). \n\n Etter godkjenning, kopier og lim inn den oppgitte PIN koden nedenfor.", + "description": "For \u00e5 koble din Nest-konto, [bekreft kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", "title": "Koble til Nest konto" } } diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index b5953decc6b..775e644a113 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_flows": "Musisz skonfigurowa\u0107 Nest, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/nest/)." }, "error": { diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index eb4ec52c0f9..8de2694095e 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -7,7 +7,7 @@ import pyatmo import requests import voluptuous as vol -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -156,7 +156,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return -class NetatmoThermostat(ClimateDevice): +class NetatmoThermostat(ClimateEntity): """Representation a Netatmo thermostat.""" def __init__(self, data, room_id): diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index ff92fd93554..ece1b33c608 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,12 +2,25 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==3.3.0"], - "after_dependencies": ["cloud"], - "dependencies": ["webhook"], - "codeowners": ["@cgtobi"], + "requirements": [ + "pyatmo==3.3.1" + ], + "after_dependencies": [ + "cloud" + ], + "dependencies": [ + "webhook" + ], + "codeowners": [ + "@cgtobi" + ], "config_flow": true, "homekit": { - "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] + "models": [ + "Healty Home Coach", + "Netatmo Relay", + "Presence", + "Welcome" + ] } -} +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index e25ca1e5849..2d41f560cff 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -1,13 +1,17 @@ { "config": { "step": { - "pick_implementation": { "title": "Pick Authentication Method" } + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } }, "abort": { - "already_setup": "You can only configure one Netatmo account.", - "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + "already_setup": "[%key:common::config_flow::abort::single_instance_allowed%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" }, - "create_entry": { "default": "Successfully authenticated with Netatmo." } + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } } } diff --git a/homeassistant/components/netatmo/translations/ca.json b/homeassistant/components/netatmo/translations/ca.json index 4cfd8e27cf7..c8638da9143 100644 --- a/homeassistant/components/netatmo/translations/ca.json +++ b/homeassistant/components/netatmo/translations/ca.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte Netatmo.", - "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component Netatmo no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "already_setup": "[%key::common::config_flow::abort::single_instance_allowed%]", + "authorize_url_timeout": "[%key::common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key::common::config_flow::abort::oauth2_missing_configuration%]" }, "create_entry": { - "default": "Autenticaci\u00f3 exitosa amb Netatmo." + "default": "[%key::common::config_flow::create_entry::authenticated%]" }, "step": { "pick_implementation": { - "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + "title": "[%key::common::config_flow::title::oauth2_pick_implementation%]" } } } diff --git a/homeassistant/components/netatmo/translations/da.json b/homeassistant/components/netatmo/translations/da.json index 1f35d3466ce..fb82b9fb642 100644 --- a/homeassistant/components/netatmo/translations/da.json +++ b/homeassistant/components/netatmo/translations/da.json @@ -1,17 +1,10 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere \u00e9n Netatmo-konto.", - "authorize_url_timeout": "Timeout ved generering af godkendelses-url.", - "missing_configuration": "Netatmo-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + "already_setup": "Du kan kun konfigurere \u00e9n Netatmo-konto." }, "create_entry": { "default": "Korrekt godkendt med Netatmo." - }, - "step": { - "pick_implementation": { - "title": "V\u00e6lg godkendelsesmetode" - } } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 0a8d187e50d..5d058f75bc4 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_setup": "Du kannst nur ein einziges Netatmo-Konto konfigurieren.", - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Netatmo-Komponente ist nicht konfiguriert. Folge bitte der Dokumentation." + "already_setup": "Bereits konfiguriert. Es ist nur eine einzige Konfiguration m\u00f6glich.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." }, "create_entry": { - "default": "Erfolgreich mit Netatmo authentifiziert." + "default": "Erfolgreich authentifiziert." }, "step": { "pick_implementation": { - "title": "W\u00e4hle die Authentifizierungsmethode" + "title": "W\u00e4hle Authentifizierungs-Methode" } } } diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json index 1e23982e0a9..15419adac40 100644 --- a/homeassistant/components/netatmo/translations/en.json +++ b/homeassistant/components/netatmo/translations/en.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_setup": "You can only configure one Netatmo account.", + "already_setup": "Already configured. Only a single configuration possible.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + "missing_configuration": "The component is not configured. Please follow the documentation." }, "create_entry": { - "default": "Successfully authenticated with Netatmo." + "default": "Successfully authenticated" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/netatmo/translations/es-419.json b/homeassistant/components/netatmo/translations/es-419.json new file mode 100644 index 00000000000..1de2505dd2d --- /dev/null +++ b/homeassistant/components/netatmo/translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta de Netatmo." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Netatmo." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index d157a746068..566225c9a9c 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_setup": "Solo puede configurar una cuenta de Netatmo.", - "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", - "missing_configuration": "El componente Netatmo no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." + "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "authorize_url_timeout": "Tiempo excedido generando la url de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, consulta la documentaci\u00f3n." }, "create_entry": { - "default": "Autenticado con \u00e9xito con Netatmo." + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + "title": "Seleccione un m\u00e9todo de autenticaci\u00f3n" } } } diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index d2d67e0e510..3dcb6bcf582 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -2,15 +2,14 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Netatmo.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "missing_configuration": "Le composant Netatmo n'est pas configur\u00e9. Veuillez suivre la documentation." + "missing_configuration": "Ce composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { "default": "Authentification r\u00e9ussie avec Netatmo." }, "step": { "pick_implementation": { - "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + "title": "Choisir une m\u00e9thode d'authentification" } } } diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index e71b6a20b24..fac2dcb754f 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -1,16 +1,10 @@ { "config": { "abort": { - "already_setup": "Csak egy Netatmo-fi\u00f3kot \u00e1ll\u00edthatsz be.", - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + "already_setup": "Csak egy Netatmo-fi\u00f3kot \u00e1ll\u00edthatsz be." }, "create_entry": { "default": "A Netatmo sikeresen hiteles\u00edtett." - }, - "step": { - "pick_implementation": { - "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" - } } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index e7d410a1d86..7095a058feb 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_setup": "\u00c8 possibile configurare un solo account Netatmo.", - "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", - "missing_configuration": "Il componente Netatmo non \u00e8 configurato. Si prega di seguire la documentazione." + "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." }, "create_entry": { - "default": "Autenticato con successo con Netatmo." + "default": "Autenticazione riuscita" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index 624298c04e5..ba274305852 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_setup": "\ud558\ub098\uc758 Netatmo \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Netatmo \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { - "default": "Netatmo \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/netatmo/translations/lb.json b/homeassistant/components/netatmo/translations/lb.json index 7332643a4f3..696844f79ae 100644 --- a/homeassistant/components/netatmo/translations/lb.json +++ b/homeassistant/components/netatmo/translations/lb.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Netatmo Kont konfigur\u00e9ieren.", - "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "Netatmo Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL", + "missing_configuration": "D\u00ebs Komponent ass net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Netatmo authentifiz\u00e9iert." }, "step": { "pick_implementation": { - "title": "Wielt Authentifikatiouns Method aus" + "title": "Authentifikatiouns Method auswielen" } } } diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index dd23b6ac429..95e8234cdb6 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -1,17 +1,10 @@ { "config": { "abort": { - "already_setup": "U kunt slechts \u00e9\u00e9n Netatmo account configureren.", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "already_setup": "U kunt slechts \u00e9\u00e9n Netatmo account configureren." }, "create_entry": { "default": "Succesvol geauthenticeerd met Netatmo." - }, - "step": { - "pick_implementation": { - "title": "Kies een authenticatie methode" - } } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 582dc1037b8..5367859668a 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -1,16 +1,15 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en Netatmo-konto.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Netatmo-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "authorize_url_timeout": "Tidsavbrutt ved oppretting av godkjennings url.", + "missing_configuration": "Komponeneten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, "create_entry": { - "default": "Vellykket autentisering med Netatmo." + "default": "Vellykket godkjenning med Netatmo." }, "step": { "pick_implementation": { - "title": "Velg autentiseringsmetode" + "title": "Velg godkjenningsmetode" } } } diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index a4d4d1b9831..718b44d60db 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Netatmo.", - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent Netatmo nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Netatmo." }, "step": { "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelniania" + "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" } } } diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index 8de4e5bcb98..a57eda48700 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Netatmo \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "step": { "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } } diff --git a/homeassistant/components/netatmo/translations/sl.json b/homeassistant/components/netatmo/translations/sl.json index 231db98bd54..7a617d5f866 100644 --- a/homeassistant/components/netatmo/translations/sl.json +++ b/homeassistant/components/netatmo/translations/sl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Konfigurirate lahko samo en ra\u010dun Netatmo.", - "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Komponenta Netatmo ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + "authorize_url_timeout": "Potekla je \u010dasovna omejitev generiranja odobritnevega URL-ja.", + "missing_configuration": "Ta komponenta ni nastavljena, prosimo sledite dokumentaciji." }, "create_entry": { "default": "Uspe\u0161no overjeno z Netatmo." }, "step": { "pick_implementation": { - "title": "Izberite na\u010din preverjanja pristnosti" + "title": "Izberite medoto za preverjanje pristnosti" } } } diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 2557123da33..365755b2806 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -1,17 +1,10 @@ { "config": { "abort": { - "already_setup": "Du kan endast konfigurera ett Netatmo-konto.", - "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", - "missing_configuration": "Netatmo-komponenten har inte konfigurerats. F\u00f6lj dokumentationen." + "already_setup": "Du kan endast konfigurera ett Netatmo-konto." }, "create_entry": { "default": "Autentiserad med Netatmo." - }, - "step": { - "pick_implementation": { - "title": "V\u00e4lj autentiseringsmetod" - } } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index eba134e1566..21d2109bfb2 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Netatmo \u5e33\u865f\u3002", + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", - "missing_configuration": "Netatmo \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Netatmo \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/netgear_lte/binary_sensor.py b/homeassistant/components/netgear_lte/binary_sensor.py index 31eb46925a1..f18c7a2ff86 100644 --- a/homeassistant/components/netgear_lte/binary_sensor.py +++ b/homeassistant/components/netgear_lte/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Netgear LTE binary sensors.""" import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity from homeassistant.exceptions import PlatformNotReady from . import CONF_MONITORED_CONDITIONS, DATA_KEY, LTEEntity @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info) async_add_entities(binary_sensors) -class LTEBinarySensor(LTEEntity, BinarySensorDevice): +class LTEBinarySensor(LTEEntity, BinarySensorEntity): """Netgear LTE binary sensor entity.""" @property diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 6baa3a63f9f..08a5b7df862 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.http import HomeAssistantView -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -125,7 +125,7 @@ class NetioApiView(HomeAssistantView): return self.json(True) -class NetioSwitch(SwitchDevice): +class NetioSwitch(SwitchEntity): """Provide a Netio linked switch.""" def __init__(self, netio, outlet, name): diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py index 5c33412c647..9adf47ee2c5 100644 --- a/homeassistant/components/nexia/binary_sensor.py +++ b/homeassistant/components/nexia/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Nexia / Trane XL Thermostats.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR from .entity import NexiaThermostatEntity @@ -34,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorDevice): +class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorEntity): """Provices Nexia BinarySensor support.""" def __init__(self, coordinator, thermostat, sensor_call, sensor_name): diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index fafa267c914..b22b185a44c 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -13,7 +13,7 @@ from nexia.const import ( ) import voluptuous as vol -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, @@ -133,7 +133,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice): +class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Provides Nexia Climate support.""" def __init__(self, coordinator, zone): diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index 2d5c5bbfb17..dcfb40b898a 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -3,7 +3,10 @@ "step": { "user": { "title": "Connect to mynexia.com", - "data": { "username": "Username", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -11,6 +14,8 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "abort": { "already_configured": "This nexia home is already configured" } + "abort": { + "already_configured": "This nexia home is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/es-419.json b/homeassistant/components/nexia/translations/es-419.json new file mode 100644 index 00000000000..e2f04c7d4b4 --- /dev/null +++ b/homeassistant/components/nexia/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Esta casa de nexia ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/hu.json b/homeassistant/components/nexia/translations/hu.json new file mode 100644 index 00000000000..dee4ed9ee0f --- /dev/null +++ b/homeassistant/components/nexia/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json new file mode 100644 index 00000000000..d718c78d7af --- /dev/null +++ b/homeassistant/components/nexia/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Deze nexia-woning is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Verbinding maken met mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json index 0819834fcc8..b6d13766dee 100644 --- a/homeassistant/components/nexia/translations/pl.json +++ b/homeassistant/components/nexia/translations/pl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Po\u0142\u0105czenie z mynexia.com" } diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json new file mode 100644 index 00000000000..9cfd620ac73 --- /dev/null +++ b/homeassistant/components/nexia/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 9e4c6f5d969..67bc580bfdf 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -1,7 +1,7 @@ """Summary binary data from Nextcoud.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import BINARY_SENSORS, DOMAIN @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors, True) -class NextcloudBinarySensor(BinarySensorDevice): +class NextcloudBinarySensor(BinarySensorEntity): """Represents a Nextcloud binary sensor.""" def __init__(self, item): diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 265e51d6e67..4875e2e1e57 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -6,7 +6,7 @@ import nikohomecontrol import voluptuous as vol # Import the device class from the component that you want to support -from homeassistant.components.light import ATTR_BRIGHTNESS, PLATFORM_SCHEMA, Light +from homeassistant.components.light import ATTR_BRIGHTNESS, PLATFORM_SCHEMA, LightEntity from homeassistant.const import CONF_HOST from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class NikoHomeControlLight(Light): +class NikoHomeControlLight(LightEntity): """Representation of an Niko Light.""" def __init__(self, light, data): diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 786d495cc9b..3d2064dad4c 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -1,7 +1,7 @@ """Plugged In Status Support for the Nissan Leaf.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN, LeafEntity @@ -22,7 +22,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class LeafPluggedInSensor(LeafEntity, BinarySensorDevice): +class LeafPluggedInSensor(LeafEntity, BinarySensorEntity): """Plugged In Sensor class.""" @property @@ -43,7 +43,7 @@ class LeafPluggedInSensor(LeafEntity, BinarySensorDevice): return "mdi:power-plug-off" -class LeafChargingSensor(LeafEntity, BinarySensorDevice): +class LeafChargingSensor(LeafEntity, BinarySensorEntity): """Charging Sensor class.""" @property diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 77ab68d8e70..258a939a07e 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -302,7 +302,9 @@ class NotionEntity(Entity): bridge_device = device_registry.async_get_device( {DOMAIN: bridge["hardware_id"]}, set() ) - this_device = device_registry.async_get_device({DOMAIN: sensor["hardware_id"]}) + this_device = device_registry.async_get_device( + {DOMAIN: sensor["hardware_id"]}, set() + ) device_registry.async_update_device( this_device.id, via_device_id=bridge_device.id diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 53a98204704..8d60ef0901a 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Notion binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from . import ( @@ -50,7 +50,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensor_list, True) -class NotionBinarySensor(NotionEntity, BinarySensorDevice): +class NotionBinarySensor(NotionEntity, BinarySensorEntity): """Define a Notion sensor.""" @property diff --git a/homeassistant/components/notion/strings.json b/homeassistant/components/notion/strings.json index 1764e6cc962..1fbe4837f7e 100644 --- a/homeassistant/components/notion/strings.json +++ b/homeassistant/components/notion/strings.json @@ -3,13 +3,18 @@ "step": { "user": { "title": "Fill in your information", - "data": { "username": "Username/Email Address", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { "invalid_credentials": "Invalid username or password", "no_devices": "No devices found in account" }, - "abort": { "already_configured": "This username is already in use." } + "abort": { + "already_configured": "This username is already in use." + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/en.json b/homeassistant/components/notion/translations/en.json index 7537482ce5e..d70aa73824a 100644 --- a/homeassistant/components/notion/translations/en.json +++ b/homeassistant/components/notion/translations/en.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Password", - "username": "Username/Email Address" + "username": "Username" }, "title": "Fill in your information" } diff --git a/homeassistant/components/notion/translations/es-419.json b/homeassistant/components/notion/translations/es-419.json index 6e0e7eb538f..a34d0356261 100644 --- a/homeassistant/components/notion/translations/es-419.json +++ b/homeassistant/components/notion/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Este nombre de usuario ya est\u00e1 en uso." + }, "error": { "invalid_credentials": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos", "no_devices": "No se han encontrado dispositivos en la cuenta." diff --git a/homeassistant/components/notion/translations/fi.json b/homeassistant/components/notion/translations/fi.json new file mode 100644 index 00000000000..e6c5220497b --- /dev/null +++ b/homeassistant/components/notion/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "T\u00e4yt\u00e4 tietosi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/ko.json b/homeassistant/components/notion/translations/ko.json index 84d55f51efc..63f1b3e767c 100644 --- a/homeassistant/components/notion/translations/ko.json +++ b/homeassistant/components/notion/translations/ko.json @@ -11,9 +11,9 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 / \uc774\uba54\uc77c \uc8fc\uc18c" + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uae30" } } } diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index b6a28495692..473659c0eb2 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Deze gebruikersnaam is al in gebruik." + }, "error": { "invalid_credentials": "Ongeldige gebruikersnaam of wachtwoord", "no_devices": "Geen apparaten gevonden in account" diff --git a/homeassistant/components/notion/translations/pl.json b/homeassistant/components/notion/translations/pl.json index 45e48d8ad33..3350a712504 100644 --- a/homeassistant/components/notion/translations/pl.json +++ b/homeassistant/components/notion/translations/pl.json @@ -10,8 +10,8 @@ "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika/adres e-mail" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]/adres e-mail" }, "title": "Wprowad\u017a dane" } diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json index 9ae52b9c797..907ee235c96 100644 --- a/homeassistant/components/notion/translations/ru.json +++ b/homeassistant/components/notion/translations/ru.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d" }, "title": "Notion" } diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index 6f9ab3bac1c..7e05095cf0e 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan." + }, "error": { "invalid_credentials": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord", "no_devices": "Inga enheter hittades p\u00e5 kontot" diff --git a/homeassistant/components/notion/translations/zh-Hant.json b/homeassistant/components/notion/translations/zh-Hant.json index ae6e6c8a619..b8713524e3c 100644 --- a/homeassistant/components/notion/translations/zh-Hant.json +++ b/homeassistant/components/notion/translations/zh-Hant.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31/\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "title": "\u586b\u5beb\u8cc7\u8a0a" } diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index c1d591c03eb..417beecee9a 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -11,7 +11,7 @@ from nuheat.util import ( nuheat_to_fahrenheit, ) -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, CURRENT_HVAC_HEAT, @@ -77,7 +77,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([entity], True) -class NuHeatThermostat(ClimateDevice): +class NuHeatThermostat(ClimateEntity): """Representation of a NuHeat Thermostat.""" def __init__(self, thermostat, temperature_unit): diff --git a/homeassistant/components/nuheat/strings.json b/homeassistant/components/nuheat/strings.json index 6a3d79e0404..367420178e6 100644 --- a/homeassistant/components/nuheat/strings.json +++ b/homeassistant/components/nuheat/strings.json @@ -6,17 +6,19 @@ "invalid_auth": "Invalid authentication", "invalid_thermostat": "The thermostat serial number is invalid." }, - "abort": { "already_configured": "The thermostat is already configured" }, + "abort": { + "already_configured": "The thermostat is already configured" + }, "step": { "user": { "title": "Connect to the NuHeat", "description": "You will need to obtain your thermostat\u2019s numeric serial number or ID by logging into https://MyNuHeat.com and selecting your thermostat(s).", "data": { - "username": "Username", - "password": "Password", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", "serial_number": "Serial number of the thermostat." } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/ca.json b/homeassistant/components/nuheat/translations/ca.json index 165d889dbdc..c7821838f4f 100644 --- a/homeassistant/components/nuheat/translations/ca.json +++ b/homeassistant/components/nuheat/translations/ca.json @@ -16,7 +16,7 @@ "serial_number": "N\u00famero de s\u00e8rie del term\u00f2stat.", "username": "Nom d'usuari" }, - "description": "Has d\u2019obtenir el n\u00famero de s\u00e8rie o identificador del teu term\u00f2stat entrant a https://MyNuHeat.com i seleccionant el teu term\u00f2stat.", + "description": "Has d'obtenir el n\u00famero de s\u00e8rie o identificador del teu term\u00f2stat entrant a https://MyNuHeat.com i seleccionant el teu term\u00f2stat.", "title": "Connexi\u00f3 amb NuHeat" } } diff --git a/homeassistant/components/nuheat/translations/es-419.json b/homeassistant/components/nuheat/translations/es-419.json new file mode 100644 index 00000000000..88e786c8b90 --- /dev/null +++ b/homeassistant/components/nuheat/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El termostato ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_thermostat": "El n\u00famero de serie del termostato no es v\u00e1lido.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "serial_number": "N\u00famero de serie del termostato.", + "username": "Nombre de usuario" + }, + "description": "Deber\u00e1 obtener el n\u00famero de serie o ID num\u00e9rico de su termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando su (s) termostato (s).", + "title": "Con\u00e9ctese a NuHeat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/hu.json b/homeassistant/components/nuheat/translations/hu.json new file mode 100644 index 00000000000..8c523b72e04 --- /dev/null +++ b/homeassistant/components/nuheat/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "A csatlakoz\u00e1s nem siker\u00fclt, pr\u00f3b\u00e1lkozzon \u00fajra", + "invalid_thermostat": "A termoszt\u00e1t sorozatsz\u00e1ma \u00e9rv\u00e9nytelen." + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "serial_number": "A termoszt\u00e1t sorozatsz\u00e1ma.", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/nl.json b/homeassistant/components/nuheat/translations/nl.json new file mode 100644 index 00000000000..edf3ad17ff4 --- /dev/null +++ b/homeassistant/components/nuheat/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "De thermostaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "invalid_thermostat": "Het serienummer van de thermostaat is ongeldig.", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "serial_number": "Serienummer van de thermostaat.", + "username": "Gebruikersnaam" + }, + "description": "U moet het numerieke serienummer of de ID van uw thermostaat verkrijgen door in te loggen op https://MyNuHeat.com en uw thermostaat (thermostaten) te selecteren.", + "title": "Maak verbinding met de NuHeat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/no.json b/homeassistant/components/nuheat/translations/no.json index 7ea083a3669..7ec631197cc 100644 --- a/homeassistant/components/nuheat/translations/no.json +++ b/homeassistant/components/nuheat/translations/no.json @@ -16,7 +16,7 @@ "serial_number": "Termostatenes serienummer.", "username": "Brukernavn" }, - "description": "Du m\u00e5 skaffe termostats numeriske serienummer eller ID ved \u00e5 logge inn p\u00e5 https://MyNuHeat.com og velge termostaten (e).", + "description": "Du m\u00e5 skaffe termostatets numeriske serienummer eller ID ved \u00e5 logge inn p\u00e5 https://MyNuHeat.com og velge termostaten (e).", "title": "Koble til NuHeat" } } diff --git a/homeassistant/components/nuheat/translations/pl.json b/homeassistant/components/nuheat/translations/pl.json index bff4192f018..d32d1f11e38 100644 --- a/homeassistant/components/nuheat/translations/pl.json +++ b/homeassistant/components/nuheat/translations/pl.json @@ -5,16 +5,16 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", "invalid_thermostat": "Numer seryjny termostatu jest nieprawid\u0142owy.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o", + "password": "[%key_id:common::config_flow::data::password%]", "serial_number": "Numer seryjny termostatu", - "username": "Nazwa u\u017cytkownika" + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Musisz uzyska\u0107 numeryczny numer seryjny lub identyfikator termostatu, loguj\u0105c si\u0119 na https://MyNuHeat.com i wybieraj\u0105c termostat(y).", "title": "Po\u0142\u0105cz z NuHeat" diff --git a/homeassistant/components/nuheat/translations/sv.json b/homeassistant/components/nuheat/translations/sv.json new file mode 100644 index 00000000000..9cfd620ac73 --- /dev/null +++ b/homeassistant/components/nuheat/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 943dbc02fbf..3d382496b28 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -6,7 +6,7 @@ from pynuki import NukiBridge from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockDevice +from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class NukiLock(LockDevice): +class NukiLock(LockEntity): """Representation of a Nuki lock.""" def __init__(self, nuki_lock): diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py new file mode 100644 index 00000000000..e5eeaa31846 --- /dev/null +++ b/homeassistant/components/numato/__init__.py @@ -0,0 +1,248 @@ +"""Support for controlling GPIO pins of a Numato Labs USB GPIO expander.""" +import logging + +import numato_gpio as gpio +import voluptuous as vol + +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_ID, + CONF_NAME, + CONF_SENSORS, + CONF_SWITCHES, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "numato" + +CONF_INVERT_LOGIC = "invert_logic" +CONF_DISCOVER = "discover" +CONF_DEVICES = "devices" +CONF_DEVICE_ID = "id" +CONF_PORTS = "ports" +CONF_SRC_RANGE = "source_range" +CONF_DST_RANGE = "destination_range" +CONF_DST_UNIT = "unit" +DEFAULT_INVERT_LOGIC = False +DEFAULT_SRC_RANGE = [0, 1024] +DEFAULT_DST_RANGE = [0.0, 100.0] +DEFAULT_UNIT = "%" +DEFAULT_DEV = [f"/dev/ttyACM{i}" for i in range(10)] + +PORT_RANGE = range(1, 8) # ports 0-7 are ADC capable + +DATA_PORTS_IN_USE = "ports_in_use" +DATA_API = "api" + + +def int_range(rng): + """Validate the input array to describe a range by two integers.""" + if not (isinstance(rng[0], int) and isinstance(rng[1], int)): + raise vol.Invalid(f"Only integers are allowed: {rng}") + if len(rng) != 2: + raise vol.Invalid(f"Only two numbers allowed in a range: {rng}") + if rng[0] > rng[1]: + raise vol.Invalid(f"Lower range bound must come first: {rng}") + return rng + + +def float_range(rng): + """Validate the input array to describe a range by two floats.""" + try: + coe = vol.Coerce(float) + coe(rng[0]) + coe(rng[1]) + except vol.CoerceInvalid: + raise vol.Invalid(f"Only int or float values are allowed: {rng}") + if len(rng) != 2: + raise vol.Invalid(f"Only two numbers allowed in a range: {rng}") + if rng[0] > rng[1]: + raise vol.Invalid(f"Lower range bound must come first: {rng}") + return rng + + +def adc_port_number(num): + """Validate input number to be in the range of ADC enabled ports.""" + try: + num = int(num) + except (ValueError): + raise vol.Invalid(f"Port numbers must be integers: {num}") + if num not in range(1, 8): + raise vol.Invalid(f"Only port numbers from 1 to 7 are ADC capable: {num}") + return num + + +ADC_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_SRC_RANGE, default=DEFAULT_SRC_RANGE): int_range, + vol.Optional(CONF_DST_RANGE, default=DEFAULT_DST_RANGE): float_range, + vol.Optional(CONF_DST_UNIT, default=DEFAULT_UNIT): cv.string, + } +) + +PORTS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) + +IO_PORTS_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORTS): PORTS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + } +) + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ID): cv.positive_int, + CONF_BINARY_SENSORS: IO_PORTS_SCHEMA, + CONF_SWITCHES: IO_PORTS_SCHEMA, + CONF_SENSORS: {CONF_PORTS: {adc_port_number: ADC_SCHEMA}}, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + CONF_DEVICES: vol.All(cv.ensure_list, [DEVICE_SCHEMA]), + vol.Optional(CONF_DISCOVER, default=DEFAULT_DEV): vol.All( + cv.ensure_list, [cv.string] + ), + }, + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Initialize the numato integration. + + Discovers available Numato devices and loads the binary_sensor, sensor and + switch platforms. + + Returns False on error during device discovery (e.g. duplicate ID), + otherwise returns True. + + No exceptions should occur, since the platforms are initialized on a best + effort basis, which means, errors are handled locally. + """ + hass.data[DOMAIN] = config[DOMAIN] + + try: + gpio.discover(config[DOMAIN][CONF_DISCOVER]) + except gpio.NumatoGpioError as err: + _LOGGER.info("Error discovering Numato devices: %s", err) + gpio.cleanup() + return False + + _LOGGER.info( + "Initializing Numato 32 port USB GPIO expanders with IDs: %s", + ", ".join(str(d) for d in gpio.devices), + ) + + hass.data[DOMAIN][DATA_API] = NumatoAPI() + + def cleanup_gpio(event): + """Stuff to do before stopping.""" + _LOGGER.debug("Clean up Numato GPIO") + gpio.cleanup() + if DATA_API in hass.data[DOMAIN]: + hass.data[DOMAIN][DATA_API].ports_registered.clear() + + def prepare_gpio(event): + """Stuff to do when home assistant starts.""" + _LOGGER.debug("Setup cleanup at stop for Numato GPIO") + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + + load_platform(hass, "binary_sensor", DOMAIN, {}, config) + load_platform(hass, "sensor", DOMAIN, {}, config) + load_platform(hass, "switch", DOMAIN, {}, config) + return True + + +# pylint: disable=no-self-use +class NumatoAPI: + """Home-Assistant specific API for numato device access.""" + + def __init__(self): + """Initialize API state.""" + self.ports_registered = dict() + + def check_port_free(self, device_id, port, direction): + """Check whether a port is still free set up. + + Fail with exception if it has already been registered. + """ + if (device_id, port) not in self.ports_registered: + self.ports_registered[(device_id, port)] = direction + else: + raise gpio.NumatoGpioError( + "Device {} port {} already in use as {}.".format( + device_id, + port, + "input" + if self.ports_registered[(device_id, port)] == gpio.IN + else "output", + ) + ) + + def check_device_id(self, device_id): + """Check whether a device has been discovered. + + Fail with exception. + """ + if device_id not in gpio.devices: + raise gpio.NumatoGpioError(f"Device {device_id} not available.") + + def check_port(self, device_id, port, direction): + """Raise an error if the port setup doesn't match the direction.""" + self.check_device_id(device_id) + if (device_id, port) not in self.ports_registered: + raise gpio.NumatoGpioError( + f"Port {port} is not set up for numato device {device_id}." + ) + msg = { + gpio.OUT: f"Trying to write to device {device_id} port {port} set up as input.", + gpio.IN: f"Trying to read from device {device_id} port {port} set up as output.", + } + if self.ports_registered[(device_id, port)] != direction: + raise gpio.NumatoGpioError(msg[direction]) + + def setup_output(self, device_id, port): + """Set up a GPIO as output.""" + self.check_device_id(device_id) + self.check_port_free(device_id, port, gpio.OUT) + gpio.devices[device_id].setup(port, gpio.OUT) + + def setup_input(self, device_id, port): + """Set up a GPIO as input.""" + self.check_device_id(device_id) + gpio.devices[device_id].setup(port, gpio.IN) + self.check_port_free(device_id, port, gpio.IN) + + def write_output(self, device_id, port, value): + """Write a value to a GPIO.""" + self.check_port(device_id, port, gpio.OUT) + gpio.devices[device_id].write(port, value) + + def read_input(self, device_id, port): + """Read a value from a GPIO.""" + self.check_port(device_id, port, gpio.IN) + return gpio.devices[device_id].read(port) + + def read_adc_input(self, device_id, port): + """Read an ADC value from a GPIO ADC port.""" + self.check_port(device_id, port, gpio.IN) + self.check_device_id(device_id) + return gpio.devices[device_id].adc_read(port) + + def edge_detect(self, device_id, port, event_callback): + """Add detection for RISING and FALLING events.""" + self.check_port(device_id, port, gpio.IN) + gpio.devices[device_id].add_event_detect(port, event_callback, gpio.BOTH) + gpio.devices[device_id].notify = True diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py new file mode 100644 index 00000000000..ff61cb3cbb0 --- /dev/null +++ b/homeassistant/components/numato/binary_sensor.py @@ -0,0 +1,120 @@ +"""Binary sensor platform integration for Numato USB GPIO expanders.""" +from functools import partial +import logging + +from numato_gpio import NumatoGpioError + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send + +from . import ( + CONF_BINARY_SENSORS, + CONF_DEVICES, + CONF_ID, + CONF_INVERT_LOGIC, + CONF_PORTS, + DATA_API, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +NUMATO_SIGNAL = "numato_signal_{}_{}" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the configured Numato USB GPIO binary sensor ports.""" + if discovery_info is None: + return + + def read_gpio(device_id, port, level): + """Send signal to entity to have it update state.""" + dispatcher_send(hass, NUMATO_SIGNAL.format(device_id, port), level) + + api = hass.data[DOMAIN][DATA_API] + binary_sensors = [] + devices = hass.data[DOMAIN][CONF_DEVICES] + for device in [d for d in devices if CONF_BINARY_SENSORS in d]: + device_id = device[CONF_ID] + platform = device[CONF_BINARY_SENSORS] + invert_logic = platform[CONF_INVERT_LOGIC] + ports = platform[CONF_PORTS] + for port, port_name in ports.items(): + try: + + api.setup_input(device_id, port) + api.edge_detect(device_id, port, partial(read_gpio, device_id)) + + except NumatoGpioError as err: + _LOGGER.error( + "Failed to initialize binary sensor '%s' on Numato device %s port %s: %s", + port_name, + device_id, + port, + err, + ) + continue + + binary_sensors.append( + NumatoGpioBinarySensor(port_name, device_id, port, invert_logic, api,) + ) + add_entities(binary_sensors, True) + + +class NumatoGpioBinarySensor(BinarySensorDevice): + """Represents a binary sensor (input) port of a Numato GPIO expander.""" + + def __init__(self, name, device_id, port, invert_logic, api): + """Initialize the Numato GPIO based binary sensor object.""" + self._name = name or DEVICE_DEFAULT_NAME + self._device_id = device_id + self._port = port + self._invert_logic = invert_logic + self._state = None + self._api = api + + async def async_added_to_hass(self): + """Connect state update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + NUMATO_SIGNAL.format(self._device_id, self._port), + self._async_update_state, + ) + ) + + @callback + def _async_update_state(self, level): + """Update entity state.""" + self._state = level + self.async_write_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the GPIO state.""" + try: + self._state = self._api.read_input(self._device_id, self._port) + except NumatoGpioError as err: + self._state = None + _LOGGER.error( + "Failed to update Numato device %s port %s: %s", + self._device_id, + self._port, + err, + ) diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json new file mode 100644 index 00000000000..4e9857cd579 --- /dev/null +++ b/homeassistant/components/numato/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "numato", + "name": "Numato USB GPIO Expander", + "documentation": "https://www.home-assistant.io/integrations/numato", + "requirements": ["numato-gpio==0.7.1"], + "codeowners": ["@clssn"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py new file mode 100644 index 00000000000..e268d32a293 --- /dev/null +++ b/homeassistant/components/numato/sensor.py @@ -0,0 +1,123 @@ +"""Sensor platform integration for ADC ports of Numato USB GPIO expanders.""" +import logging + +from numato_gpio import NumatoGpioError + +from homeassistant.const import CONF_ID, CONF_NAME, CONF_SENSORS +from homeassistant.helpers.entity import Entity + +from . import ( + CONF_DEVICES, + CONF_DST_RANGE, + CONF_DST_UNIT, + CONF_PORTS, + CONF_SRC_RANGE, + DATA_API, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +ICON = "mdi:gauge" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the configured Numato USB GPIO ADC sensor ports.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN][DATA_API] + sensors = [] + devices = hass.data[DOMAIN][CONF_DEVICES] + for device in [d for d in devices if CONF_SENSORS in d]: + device_id = device[CONF_ID] + ports = device[CONF_SENSORS][CONF_PORTS] + for port, adc_def in ports.items(): + try: + api.setup_input(device_id, port) + except NumatoGpioError as err: + _LOGGER.error( + "Failed to initialize sensor '%s' on Numato device %s port %s: %s", + adc_def[CONF_NAME], + device_id, + port, + err, + ) + continue + sensors.append( + NumatoGpioAdc( + adc_def[CONF_NAME], + device_id, + port, + adc_def[CONF_SRC_RANGE], + adc_def[CONF_DST_RANGE], + adc_def[CONF_DST_UNIT], + api, + ) + ) + add_entities(sensors, True) + + +class NumatoGpioAdc(Entity): + """Represents an ADC port of a Numato USB GPIO expander.""" + + def __init__(self, name, device_id, port, src_range, dst_range, dst_unit, api): + """Initialize the sensor.""" + self._name = name + self._device_id = device_id + self._port = port + self._src_range = src_range + self._dst_range = dst_range + self._state = None + self._unit_of_measurement = dst_unit + self._api = api + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data and updates the state.""" + try: + adc_val = self._api.read_adc_input(self._device_id, self._port) + adc_val = self._clamp_to_source_range(adc_val) + self._state = self._linear_scale_to_dest_range(adc_val) + except NumatoGpioError as err: + self._state = None + _LOGGER.error( + "Failed to update Numato device %s ADC-port %s: %s", + self._device_id, + self._port, + err, + ) + + def _clamp_to_source_range(self, val): + # clamp to source range + val = max(val, self._src_range[0]) + val = min(val, self._src_range[1]) + return val + + def _linear_scale_to_dest_range(self, val): + # linear scale to dest range + src_len = self._src_range[1] - self._src_range[0] + adc_val_rel = val - self._src_range[0] + ratio = float(adc_val_rel) / float(src_len) + dst_len = self._dst_range[1] - self._dst_range[0] + dest_val = self._dst_range[0] + ratio * dst_len + return dest_val diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py new file mode 100644 index 00000000000..2f1be0cf311 --- /dev/null +++ b/homeassistant/components/numato/switch.py @@ -0,0 +1,108 @@ +"""Switch platform integration for Numato USB GPIO expanders.""" +import logging + +from numato_gpio import NumatoGpioError + +from homeassistant.const import ( + CONF_DEVICES, + CONF_ID, + CONF_SWITCHES, + DEVICE_DEFAULT_NAME, +) +from homeassistant.helpers.entity import ToggleEntity + +from . import CONF_INVERT_LOGIC, CONF_PORTS, DATA_API, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the configured Numato USB GPIO switch ports.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN][DATA_API] + switches = [] + devices = hass.data[DOMAIN][CONF_DEVICES] + for device in [d for d in devices if CONF_SWITCHES in d]: + device_id = device[CONF_ID] + platform = device[CONF_SWITCHES] + invert_logic = platform[CONF_INVERT_LOGIC] + ports = platform[CONF_PORTS] + for port, port_name in ports.items(): + try: + api.setup_output(device_id, port) + api.write_output(device_id, port, 1 if invert_logic else 0) + except NumatoGpioError as err: + _LOGGER.error( + "Failed to initialize switch '%s' on Numato device %s port %s: %s", + port_name, + device_id, + port, + err, + ) + continue + switches.append( + NumatoGpioSwitch(port_name, device_id, port, invert_logic, api,) + ) + add_entities(switches, True) + + +class NumatoGpioSwitch(ToggleEntity): + """Representation of a Numato USB GPIO switch port.""" + + def __init__(self, name, device_id, port, invert_logic, api): + """Initialize the port.""" + self._name = name or DEVICE_DEFAULT_NAME + self._device_id = device_id + self._port = port + self._invert_logic = invert_logic + self._state = False + self._api = api + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if port is turned on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the port on.""" + try: + self._api.write_output( + self._device_id, self._port, 0 if self._invert_logic else 1 + ) + self._state = True + self.schedule_update_ha_state() + except NumatoGpioError as err: + _LOGGER.error( + "Failed to turn on Numato device %s port %s: %s", + self._device_id, + self._port, + err, + ) + + def turn_off(self, **kwargs): + """Turn the port off.""" + try: + self._api.write_output( + self._device_id, self._port, 1 if self._invert_logic else 0 + ) + self._state = False + self.schedule_update_ha_state() + except NumatoGpioError as err: + _LOGGER.error( + "Failed to turn off Numato device %s port %s: %s", + self._device_id, + self._port, + err, + ) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 99563ca65d4..5669b8a5c3b 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( COORDINATOR, @@ -61,7 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data(): """Fetch data from NUT.""" async with async_timeout.timeout(10): - return await hass.async_add_executor_job(data.update) + await hass.async_add_executor_job(data.update) + if not data.status: + raise UpdateFailed("Error fetching UPS state") coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 32daaaa2582..3c2144a0aee 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -189,6 +189,8 @@ class NUTSensor(Entity): @property def state(self): """Return entity state from ups.""" + if not self._data.status: + return None if self._type == KEY_STATUS_DISPLAY: return _format_display_state(self._data.status) return self._data.status.get(self._type) diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index 76c12cdacfe..2203a501ff2 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -4,26 +4,33 @@ "user": { "title": "Connect to the NUT server", "data": { - "host": "Host", - "port": "Port", - "username": "Username", - "password": "Password" + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, "ups": { "title": "Choose the UPS to Monitor", - "data": { "alias": "Alias", "resources": "Resources" } + "data": { + "alias": "Alias", + "resources": "Resources" + } }, "resources": { "title": "Choose the Resources to Monitor", - "data": { "resources": "Resources" } + "data": { + "resources": "Resources" + } } }, "error": { "cannot_connect": "Failed to connect, please try again", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } }, "options": { "step": { @@ -36,4 +43,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/es-419.json b/homeassistant/components/nut/translations/es-419.json new file mode 100644 index 00000000000..b33bc854b23 --- /dev/null +++ b/homeassistant/components/nut/translations/es-419.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "resources": { + "data": { + "resources": "Recursos" + }, + "title": "Seleccione los recursos para monitorear" + }, + "ups": { + "data": { + "alias": "Alias", + "resources": "Recursos" + }, + "title": "Seleccione el UPS para monitorear" + }, + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese al servidor NUT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Recursos", + "scan_interval": "Intervalo de escaneo (segundos)" + }, + "description": "Seleccione los Recursos del sensor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index 81b0f9c8b71..d05fb5587dc 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -39,7 +39,7 @@ "resources": "Recursos", "scan_interval": "Intervalo de escaneo (segundos)" }, - "description": "Elegir Recursos del Sensor" + "description": "Elige los recursos del sensor." } } } diff --git a/homeassistant/components/nut/translations/fr.json b/homeassistant/components/nut/translations/fr.json index 02927228c3f..c2f3793d744 100644 --- a/homeassistant/components/nut/translations/fr.json +++ b/homeassistant/components/nut/translations/fr.json @@ -27,7 +27,8 @@ "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" - } + }, + "title": "Se connecter au serveur NUT" } } }, @@ -35,7 +36,8 @@ "step": { "init": { "data": { - "resources": "Ressources" + "resources": "Ressources", + "scan_interval": "Intervalle de balayage (secondes)" } } } diff --git a/homeassistant/components/nut/translations/ko.json b/homeassistant/components/nut/translations/ko.json index 6a74c3969b6..81fe8d88a90 100644 --- a/homeassistant/components/nut/translations/ko.json +++ b/homeassistant/components/nut/translations/ko.json @@ -12,14 +12,14 @@ "data": { "resources": "\ub9ac\uc18c\uc2a4" }, - "title": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \ub9ac\uc18c\uc2a4 \uc120\ud0dd" + "title": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \ub9ac\uc18c\uc2a4 \uc120\ud0dd\ud558\uae30" }, "ups": { "data": { "alias": "\ubcc4\uba85", "resources": "\ub9ac\uc18c\uc2a4" }, - "title": "\ubaa8\ub2c8\ud130\ub9c1\ud560 UPS \uc120\ud0dd" + "title": "\ubaa8\ub2c8\ud130\ub9c1\ud560 UPS \uc120\ud0dd\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/nut/translations/nl.json b/homeassistant/components/nut/translations/nl.json index 2eaad319712..5e4acf3574d 100644 --- a/homeassistant/components/nut/translations/nl.json +++ b/homeassistant/components/nut/translations/nl.json @@ -1,10 +1,24 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", "unknown": "Onverwachte fout" }, "step": { + "resources": { + "data": { + "resources": "Bronnen" + }, + "title": "Kies de te controleren bronnen" + }, "ups": { + "data": { + "alias": "Alias", + "resources": "Bronnen" + }, "title": "Kies een UPS om uit te lezen" }, "user": { @@ -22,8 +36,10 @@ "step": { "init": { "data": { - "resources": "Bronnen" - } + "resources": "Bronnen", + "scan_interval": "Scaninterval (seconden)" + }, + "description": "Kies Sensorbronnen." } } } diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json index 5464e034244..de43f9ead89 100644 --- a/homeassistant/components/nut/translations/no.json +++ b/homeassistant/components/nut/translations/no.json @@ -16,7 +16,7 @@ }, "ups": { "data": { - "alias": "Alias", + "alias": "", "resources": "Ressurser" }, "title": "Velg UPS som skal overv\u00e5kes" diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json index 32fab6f325b..282d1e15ab6 100644 --- a/homeassistant/components/nut/translations/pl.json +++ b/homeassistant/components/nut/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "resources": { @@ -23,10 +23,10 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika" + "host": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Po\u0142\u0105cz z serwerem NUT" } diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json new file mode 100644 index 00000000000..b8673b1b8f9 --- /dev/null +++ b/homeassistant/components/nut/translations/sv.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "resources": { + "data": { + "resources": "Resurser" + }, + "title": "V\u00e4lj resurser som ska \u00f6vervakas" + }, + "ups": { + "data": { + "alias": "Alias" + } + }, + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index f6cdc7c57cd..9f0579dc20e 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -4,13 +4,12 @@ import datetime import logging from pynws import SimpleNWS -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -24,27 +23,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -_INDIVIDUAL_SCHEMA = vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - vol.Optional(CONF_STATION): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [_INDIVIDUAL_SCHEMA])}, extra=vol.ALLOW_EXTRA, -) - PLATFORMS = ["weather"] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) +DEBOUNCE_TIME = 60 # in seconds + def base_unique_id(latitude, longitude): """Return unique id for entries in configuration.""" @@ -75,6 +59,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name=f"NWS observation station {station}", update_method=nws_data.update_observation, update_interval=DEFAULT_SCAN_INTERVAL, + request_refresh_debouncer=debounce.Debouncer( + hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True + ), ) coordinator_forecast = DataUpdateCoordinator( @@ -83,6 +70,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name=f"NWS forecast station {station}", update_method=nws_data.update_forecast, update_interval=DEFAULT_SCAN_INTERVAL, + request_refresh_debouncer=debounce.Debouncer( + hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True + ), ) coordinator_forecast_hourly = DataUpdateCoordinator( @@ -91,6 +81,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name=f"NWS forecast hourly station {station}", update_method=nws_data.update_forecast_hourly, update_interval=DEFAULT_SCAN_INTERVAL, + request_refresh_debouncer=debounce.Debouncer( + hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True + ), ) nws_hass_data = hass.data.setdefault(DOMAIN, {}) nws_hass_data[entry.entry_id] = { diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 2aa783f7a28..da465b0eea5 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nws", "codeowners": ["@MatthewFlamm"], "requirements": ["pynws==0.10.4"], - "quality_scale": "silver", + "quality_scale": "platinum", "config_flow": true } diff --git a/homeassistant/components/nws/strings.json b/homeassistant/components/nws/strings.json index 4d9783ca1a0..83e8a3a694b 100644 --- a/homeassistant/components/nws/strings.json +++ b/homeassistant/components/nws/strings.json @@ -5,7 +5,7 @@ "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.", "title": "Connect to the National Weather Service", "data": { - "api_key": "API key (email)", + "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "Latitude", "longitude": "Longitude", "station": "METAR station code" @@ -16,6 +16,8 @@ "cannot_connect": "Failed to connect, please try again", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/en.json b/homeassistant/components/nws/translations/en.json index 929281f736d..19eec6a20e3 100644 --- a/homeassistant/components/nws/translations/en.json +++ b/homeassistant/components/nws/translations/en.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API key (email)", + "api_key": "API Key", "latitude": "Latitude", "longitude": "Longitude", "station": "METAR station code" diff --git a/homeassistant/components/nws/translations/es-419.json b/homeassistant/components/nws/translations/es-419.json new file mode 100644 index 00000000000..a44b2899e3d --- /dev/null +++ b/homeassistant/components/nws/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API (correo electr\u00f3nico)", + "latitude": "Latitud", + "longitude": "Longitud", + "station": "C\u00f3digo de estaci\u00f3n METAR" + }, + "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana.", + "title": "Con\u00e9ctese al Servicio Meteorol\u00f3gico Nacional" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/fi.json b/homeassistant/components/nws/translations/fi.json new file mode 100644 index 00000000000..5c3329a260a --- /dev/null +++ b/homeassistant/components/nws/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "unknown": "Odottamaton virhe" + }, + "step": { + "user": { + "data": { + "api_key": "API-avain (s\u00e4hk\u00f6posti)", + "latitude": "Leveysaste", + "longitude": "Pituusaste" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/fr.json b/homeassistant/components/nws/translations/fr.json index 94bb529aa62..59fd1d9fedb 100644 --- a/homeassistant/components/nws/translations/fr.json +++ b/homeassistant/components/nws/translations/fr.json @@ -10,8 +10,10 @@ "step": { "user": { "data": { + "api_key": "Cl\u00e9 API (e-mail)", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "station": "Code de la station METAR" } } } diff --git a/homeassistant/components/nws/translations/ko.json b/homeassistant/components/nws/translations/ko.json index 3b6eae14ba7..552099c7193 100644 --- a/homeassistant/components/nws/translations/ko.json +++ b/homeassistant/components/nws/translations/ko.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API \ud0a4 (\uc774\uba54\uc77c)", + "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", "station": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc" diff --git a/homeassistant/components/nws/translations/nl.json b/homeassistant/components/nws/translations/nl.json index 590b9c90e12..b74e6db96a2 100644 --- a/homeassistant/components/nws/translations/nl.json +++ b/homeassistant/components/nws/translations/nl.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, "step": { "user": { "data": { - "longitude": "Lengtegraad" - } + "api_key": "API-sleutel (e-mail)", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "station": "METAR-zendercode" + }, + "description": "Als er geen METAR-zendercode is opgegeven, worden de lengte- en breedtegraad gebruikt om het dichtstbijzijnde station te vinden.", + "title": "Maak verbinding met de National Weather Service" } } } diff --git a/homeassistant/components/nws/translations/pl.json b/homeassistant/components/nws/translations/pl.json index 3da4d1f3ea8..ad94c694837 100644 --- a/homeassistant/components/nws/translations/pl.json +++ b/homeassistant/components/nws/translations/pl.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "api_key": "Klucz API (e-mail)", + "api_key": "[%key_id:common::config_flow::data::api_key%] (e-mail)", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "station": "Kod stacji METAR" }, - "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte szeroko\u015b\u0107 i d\u0142ugo\u015b\u0107 geograficzna.", + "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte wsp\u00f3\u0142rz\u0119dne geograficzne.", "title": "Po\u0142\u0105cz z National Weather Service" } } diff --git a/homeassistant/components/nws/translations/ru.json b/homeassistant/components/nws/translations/ru.json index a5808a43f12..96ab63cd2d4 100644 --- a/homeassistant/components/nws/translations/ru.json +++ b/homeassistant/components/nws/translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API (\u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b)", + "api_key": "\u041a\u043b\u044e\u0447 API", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "station": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR" diff --git a/homeassistant/components/nws/translations/sl.json b/homeassistant/components/nws/translations/sl.json new file mode 100644 index 00000000000..bd49bfd1b13 --- /dev/null +++ b/homeassistant/components/nws/translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "api_key": "Klju\u010d API (e-po\u0161ta)", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "station": "Koda postaje METAR" + }, + "description": "\u010ce koda postaje METAR ni dolo\u010dena, se za iskanje najbli\u017eje postaje uporabita zemljepisna \u0161irina in dol\u017eina.", + "title": "Pove\u017eite se z nacionalno vremensko slu\u017ebo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/sv.json b/homeassistant/components/nws/translations/sv.json new file mode 100644 index 00000000000..fefa01fd40a --- /dev/null +++ b/homeassistant/components/nws/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/zh-Hant.json b/homeassistant/components/nws/translations/zh-Hant.json index 1b614a752d9..703dbd240b8 100644 --- a/homeassistant/components/nws/translations/zh-Hant.json +++ b/homeassistant/components/nws/translations/zh-Hant.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API \u5bc6\u9470\uff08\u90f5\u4ef6\uff09", + "api_key": "API \u5bc6\u9470", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "station": "METAR \u6a5f\u5834\u4ee3\u78bc" diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index afccebfbccd..807591e0c2b 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -45,6 +45,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + def convert_condition(time, weather): """ @@ -289,3 +291,11 @@ class NWSWeather(WeatherEntity): self.coordinator_observation.last_update_success and self.coordinator_forecast.last_update_success ) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator_observation.async_request_refresh() + await self.coordinator_forecast.async_request_refresh() diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 7a064ef0d00..bc2c5034ed1 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return -class NX584Alarm(alarm.AlarmControlPanel): +class NX584Alarm(alarm.AlarmControlPanelEntity): """Representation of a NX584-based alarm panel.""" def __init__(self, hass, url, name): diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index f6006ff2de4..d12f337c171 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -72,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class NX584ZoneSensor(BinarySensorDevice): +class NX584ZoneSensor(BinarySensorEntity): """Representation of a NX584 zone as a sensor.""" def __init__(self, zone, zone_type): diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index 7ed1170c6a0..0f740525f84 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -3,7 +3,7 @@ import logging import requests -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import BINARY_SENSOR_TYPES, DOMAIN as COMPONENT_DOMAIN @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class OctoPrintBinarySensor(BinarySensorDevice): +class OctoPrintBinarySensor(BinarySensorEntity): """Representation an OctoPrint binary sensor.""" def __init__( diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index b2ecf5a7997..734fe12d0f5 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -5,7 +5,7 @@ from oemthermostat import Thermostat import requests import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities((ThermostatDevice(therm, name),), True) -class ThermostatDevice(ClimateDevice): +class ThermostatDevice(ClimateEntity): """Interface class for the oemthermostat module.""" def __init__(self, thermostat, name): diff --git a/homeassistant/components/onboarding/translations/fi.json b/homeassistant/components/onboarding/translations/fi.json new file mode 100644 index 00000000000..312f0496520 --- /dev/null +++ b/homeassistant/components/onboarding/translations/fi.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Makuuhuone", + "kitchen": "Keitti\u00f6", + "living_room": "Olohuone" + } +} \ No newline at end of file diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 93107b2eb48..30f4ae0800a 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -6,7 +6,7 @@ import eiscp from eiscp import eISCP import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_PLAY, @@ -211,7 +211,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(hosts, True) -class OnkyoDevice(MediaPlayerDevice): +class OnkyoDevice(MediaPlayerEntity): """Representation of an Onkyo device.""" def __init__( diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index ea4c875ac20..6d90c5828f9 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1 +1,122 @@ -"""The onvif component.""" +"""The ONVIF integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_per_platform + +from .const import ( + CONF_RTSP_TRANSPORT, + DEFAULT_ARGUMENTS, + DEFAULT_NAME, + DEFAULT_PASSWORD, + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + RTSP_TRANS_PROTOCOLS, +) +from .device import ONVIFDevice + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the ONVIF component.""" + # Import from yaml + configs = {} + for p_type, p_config in config_per_platform(config, "camera"): + if p_type != DOMAIN: + continue + + config = p_config.copy() + if config[CONF_HOST] not in configs.keys(): + configs[config[CONF_HOST]] = { + CONF_HOST: config[CONF_HOST], + CONF_NAME: config.get(CONF_NAME, DEFAULT_NAME), + CONF_PASSWORD: config.get(CONF_PASSWORD, DEFAULT_PASSWORD), + CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), + CONF_USERNAME: config.get(CONF_USERNAME, DEFAULT_USERNAME), + } + + for conf in configs.values(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up ONVIF from a config entry.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + if not entry.options: + await async_populate_options(hass, entry) + + device = ONVIFDevice(hass, entry) + + if not await device.async_setup(): + return False + + if not device.available: + raise ConfigEntryNotReady() + + hass.data[DOMAIN][entry.unique_id] = device + + platforms = ["camera"] + + if device.capabilities.events and await device.events.async_start(): + platforms += ["binary_sensor", "sensor"] + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.events.async_stop) + + for component in platforms: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + device = hass.data[DOMAIN][entry.unique_id] + platforms = ["camera"] + + if device.capabilities.events and device.events.started: + platforms += ["binary_sensor", "sensor"] + await device.events.async_stop() + + return all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in platforms + ] + ) + ) + + +async def async_populate_options(hass, entry): + """Populate default options for device.""" + options = { + CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS, + CONF_RTSP_TRANSPORT: RTSP_TRANS_PROTOCOLS[0], + } + + hass.config_entries.async_update_entry(entry, options=options) diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py new file mode 100644 index 00000000000..43a846cac37 --- /dev/null +++ b/homeassistant/components/onvif/base.py @@ -0,0 +1,31 @@ +"""Base classes for ONVIF entities.""" +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import Entity + +from .device import ONVIFDevice +from .models import Profile + + +class ONVIFBaseEntity(Entity): + """Base class common to all ONVIF entities.""" + + def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None: + """Initialize the ONVIF entity.""" + self.device = device + self.profile = profile + + @property + def available(self): + """Return True if device is available.""" + return self.device.available + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self.device.info.mac)}, + "manufacturer": self.device.info.manufacturer, + "model": self.device.info.model, + "name": self.device.name, + "sw_version": self.device.info.fw_version, + } diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py new file mode 100644 index 00000000000..9b5469ee0d0 --- /dev/null +++ b/homeassistant/components/onvif/binary_sensor.py @@ -0,0 +1,84 @@ +"""Support for ONVIF binary sensors.""" +from typing import Optional + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback + +from .base import ONVIFBaseEntity +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a ONVIF binary sensor.""" + device = hass.data[DOMAIN][config_entry.unique_id] + + entities = { + event.uid: ONVIFBinarySensor(event.uid, device) + for event in device.events.get_platform("binary_sensor") + } + + async_add_entities(entities.values()) + + @callback + def async_check_entities(): + """Check if we have added an entity for the event.""" + new_entities = [] + for event in device.events.get_platform("binary_sensor"): + if event.uid not in entities: + entities[event.uid] = ONVIFBinarySensor(event.uid, device) + new_entities.append(entities[event.uid]) + async_add_entities(new_entities) + + device.events.async_add_listener(async_check_entities) + + return True + + +class ONVIFBinarySensor(ONVIFBaseEntity, BinarySensorEntity): + """Representation of a binary ONVIF event.""" + + def __init__(self, uid, device): + """Initialize the ONVIF binary sensor.""" + ONVIFBaseEntity.__init__(self, device) + BinarySensorEntity.__init__(self) + + self.uid = uid + + @property + def is_on(self) -> bool: + """Return true if event is active.""" + return self.device.events.get_uid(self.uid).value + + @property + def name(self) -> str: + """Return the name of the event.""" + return self.device.events.get_uid(self.uid).name + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self.device.events.get_uid(self.uid).device_class + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self.uid + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self.device.events.get_uid(self.uid).entity_enabled + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.device.events.async_add_listener(self.async_write_ha_state) + ) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 51ea3fab0cc..4d39c95c3cd 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,547 +1,158 @@ """Support for ONVIF Cameras with FFmpeg as decoder.""" import asyncio -import datetime as dt -import logging -import os -from typing import Optional -from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame -import onvif -from onvif import ONVIFCamera, exceptions import requests from requests.auth import HTTPDigestAuth import voluptuous as vol -from zeep.asyncio import AsyncTransport -from zeep.exceptions import Fault -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import ( - async_aiohttp_proxy_stream, - async_get_clientsession, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.service import async_extract_entity_ids -import homeassistant.util.dt as dt_util +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "ONVIF Camera" -DEFAULT_PORT = 5000 -DEFAULT_USERNAME = "admin" -DEFAULT_PASSWORD = "888888" -DEFAULT_ARGUMENTS = "-pred 1" -DEFAULT_PROFILE = 0 - -CONF_PROFILE = "profile" -CONF_RTSP_TRANSPORT = "rtsp_transport" - -ATTR_PAN = "pan" -ATTR_TILT = "tilt" -ATTR_ZOOM = "zoom" -ATTR_DISTANCE = "distance" -ATTR_SPEED = "speed" -ATTR_MOVE_MODE = "move_mode" -ATTR_CONTINUOUS_DURATION = "continuous_duration" - -DIR_UP = "UP" -DIR_DOWN = "DOWN" -DIR_LEFT = "LEFT" -DIR_RIGHT = "RIGHT" -ZOOM_OUT = "ZOOM_OUT" -ZOOM_IN = "ZOOM_IN" -PAN_FACTOR = {DIR_RIGHT: 1, DIR_LEFT: -1} -TILT_FACTOR = {DIR_UP: 1, DIR_DOWN: -1} -ZOOM_FACTOR = {ZOOM_IN: 1, ZOOM_OUT: -1} -CONTINUOUS_MOVE = "ContinuousMove" -RELATIVE_MOVE = "RelativeMove" -ABSOLUTE_MOVE = "AbsoluteMove" - -SERVICE_PTZ = "ptz" - -DOMAIN = "onvif" -ONVIF_DATA = "onvif" -ENTITIES = "entities" - -RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, - vol.Optional(CONF_RTSP_TRANSPORT, default=RTSP_TRANS_PROTOCOLS[0]): vol.In( - RTSP_TRANS_PROTOCOLS - ), - vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): vol.All( - vol.Coerce(int), vol.Range(min=0) - ), - } -) - -SERVICE_PTZ_SCHEMA = vol.Schema( - { - ATTR_ENTITY_ID: cv.entity_ids, - vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]), - vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]), - vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]), - ATTR_MOVE_MODE: vol.In([CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE]), - vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float, - vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, - vol.Optional(ATTR_SPEED, default=0.5): cv.small_float, - } +from .base import ONVIFBaseEntity +from .const import ( + ABSOLUTE_MOVE, + ATTR_CONTINUOUS_DURATION, + ATTR_DISTANCE, + ATTR_MOVE_MODE, + ATTR_PAN, + ATTR_PRESET, + ATTR_SPEED, + ATTR_TILT, + ATTR_ZOOM, + CONF_RTSP_TRANSPORT, + CONTINUOUS_MOVE, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + DIR_UP, + DOMAIN, + GOTOPRESET_MOVE, + LOGGER, + RELATIVE_MOVE, + SERVICE_PTZ, + ZOOM_IN, + ZOOM_OUT, ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a ONVIF camera.""" - _LOGGER.debug("Setting up the ONVIF camera platform") +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ONVIF camera video stream.""" + platform = entity_platform.current_platform.get() - async def async_handle_ptz(service): - """Handle PTZ service call.""" - pan = service.data.get(ATTR_PAN) - tilt = service.data.get(ATTR_TILT) - zoom = service.data.get(ATTR_ZOOM) - distance = service.data[ATTR_DISTANCE] - speed = service.data[ATTR_SPEED] - move_mode = service.data.get(ATTR_MOVE_MODE) - continuous_duration = service.data[ATTR_CONTINUOUS_DURATION] - all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = await async_extract_entity_ids(hass, service) - target_cameras = [] - if not entity_ids: - target_cameras = all_cameras - else: - target_cameras = [ - camera for camera in all_cameras if camera.entity_id in entity_ids - ] - for camera in target_cameras: - await camera.async_perform_ptz( - pan, tilt, zoom, distance, speed, move_mode, continuous_duration - ) - - hass.services.async_register( - DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA + # Create PTZ service + platform.async_register_entity_service( + SERVICE_PTZ, + { + vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]), + vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]), + vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]), + vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, + vol.Optional(ATTR_SPEED, default=0.5): cv.small_float, + vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In( + [CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE, GOTOPRESET_MOVE] + ), + vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float, + vol.Optional(ATTR_PRESET, default="0"): cv.string, + }, + "async_perform_ptz", ) - _LOGGER.debug("Constructing the ONVIFHassCamera") + device = hass.data[DOMAIN][config_entry.unique_id] + async_add_entities( + [ONVIFCameraEntity(device, profile) for profile in device.profiles] + ) - hass_camera = ONVIFHassCamera(hass, config) - - await hass_camera.async_initialize() - - async_add_entities([hass_camera]) - return + return True -class ONVIFHassCamera(Camera): - """An implementation of an ONVIF camera.""" +class ONVIFCameraEntity(ONVIFBaseEntity, Camera): + """Representation of an ONVIF camera.""" - def __init__(self, hass, config): - """Initialize an ONVIF camera.""" - super().__init__() - - _LOGGER.debug("Importing dependencies") - - _LOGGER.debug("Setting up the ONVIF camera component") - - self._username = config.get(CONF_USERNAME) - self._password = config.get(CONF_PASSWORD) - self._host = config.get(CONF_HOST) - self._port = config.get(CONF_PORT) - self._name = config.get(CONF_NAME) - self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) - self._profile_index = config.get(CONF_PROFILE) - self._ptz_service = None - self._input = None - self._snapshot = None - self.stream_options[CONF_RTSP_TRANSPORT] = config.get(CONF_RTSP_TRANSPORT) - self._mac = None - - _LOGGER.debug( - "Setting up the ONVIF camera device @ '%s:%s'", self._host, self._port + def __init__(self, device, profile): + """Initialize ONVIF camera entity.""" + ONVIFBaseEntity.__init__(self, device, profile) + Camera.__init__(self) + self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( + CONF_RTSP_TRANSPORT ) + self._stream_uri = None + self._snapshot_uri = None - session = async_get_clientsession(hass) - transport = AsyncTransport(None, session=session) - self._camera = ONVIFCamera( - self._host, - self._port, - self._username, - self._password, - f"{os.path.dirname(onvif.__file__)}/wsdl/", - transport=transport, - ) + @property + def supported_features(self) -> int: + """Return supported features.""" + return SUPPORT_STREAM - async def async_initialize(self): - """ - Initialize the camera. + @property + def name(self) -> str: + """Return the name of this camera.""" + return f"{self.device.name} - {self.profile.name}" - Initializes the camera by obtaining the input uri and connecting to - the camera. Also retrieves the ONVIF profiles. - """ - try: - _LOGGER.debug("Updating service addresses") - await self._camera.update_xaddrs() + @property + def unique_id(self) -> str: + """Return a unique ID.""" + if self.profile.index: + return f"{self.device.info.mac}_{self.profile.index}" + return self.device.info.mac - await self.async_obtain_mac_address() - await self.async_check_date_and_time() - await self.async_obtain_input_uri() - await self.async_obtain_snapshot_uri() - self.setup_ptz() - except ClientConnectionError as err: - _LOGGER.warning( - "Couldn't connect to camera '%s', but will retry later. Error: %s", - self._name, - err, + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self.device.max_resolution == self.profile.video.resolution.width + + async def stream_source(self): + """Return the stream source.""" + if self._stream_uri is None: + uri_no_auth = await self.device.async_get_stream_uri(self.profile) + self._stream_uri = uri_no_auth.replace( + "rtsp://", f"rtsp://{self.device.username}:{self.device.password}@", 1 ) - raise PlatformNotReady - except Fault as err: - _LOGGER.error( - "Couldn't connect to camera '%s', please verify " - "that the credentials are correct. Error: %s", - self._name, - err, - ) - - async def async_obtain_mac_address(self): - """Obtain the MAC address of the camera to use as the unique ID.""" - devicemgmt = self._camera.create_devicemgmt_service() - network_interfaces = await devicemgmt.GetNetworkInterfaces() - for interface in network_interfaces: - if interface.Enabled: - self._mac = interface.Info.HwAddress - - async def async_check_date_and_time(self): - """Warns if camera and system date not synced.""" - _LOGGER.debug("Setting up the ONVIF device management service") - devicemgmt = self._camera.create_devicemgmt_service() - - _LOGGER.debug("Retrieving current camera date/time") - try: - system_date = dt_util.utcnow() - device_time = await devicemgmt.GetSystemDateAndTime() - if not device_time: - _LOGGER.debug( - """Couldn't get camera '%s' date/time. - GetSystemDateAndTime() return null/empty""", - self._name, - ) - return - - if device_time.UTCDateTime: - tzone = dt_util.UTC - cdate = device_time.UTCDateTime - else: - tzone = ( - dt_util.get_time_zone(device_time.TimeZone) - or dt_util.DEFAULT_TIME_ZONE - ) - cdate = device_time.LocalDateTime - - if cdate is None: - _LOGGER.warning("Could not retrieve date/time on this camera") - else: - cam_date = dt.datetime( - cdate.Date.Year, - cdate.Date.Month, - cdate.Date.Day, - cdate.Time.Hour, - cdate.Time.Minute, - cdate.Time.Second, - 0, - tzone, - ) - - cam_date_utc = cam_date.astimezone(dt_util.UTC) - - _LOGGER.debug("TimeZone for date/time: %s", tzone) - - _LOGGER.debug("Camera date/time: %s", cam_date) - - _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc) - - _LOGGER.debug("System date/time: %s", system_date) - - dt_diff = cam_date - system_date - dt_diff_seconds = dt_diff.total_seconds() - - if dt_diff_seconds > 5: - _LOGGER.warning( - "The date/time on the camera (UTC) is '%s', " - "which is different from the system '%s', " - "this could lead to authentication issues", - cam_date_utc, - system_date, - ) - except ServerDisconnectedError as err: - _LOGGER.warning( - "Couldn't get camera '%s' date/time. Error: %s", self._name, err - ) - - async def async_obtain_profile_token(self): - """Obtain profile token to use with requests.""" - try: - media_service = self._camera.get_service("media") - - profiles = await media_service.GetProfiles() - - _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) - - if self._profile_index >= len(profiles): - _LOGGER.warning( - "ONVIF Camera '%s' doesn't provide profile %d." - " Using the last profile.", - self._name, - self._profile_index, - ) - self._profile_index = -1 - - _LOGGER.debug("Using profile index '%d'", self._profile_index) - - return profiles[self._profile_index].token - except exceptions.ONVIFError as err: - _LOGGER.error( - "Couldn't retrieve profile token of camera '%s'. Error: %s", - self._name, - err, - ) - return None - - async def async_obtain_input_uri(self): - """Set the input uri for the camera.""" - _LOGGER.debug( - "Connecting with ONVIF Camera: %s on port %s", self._host, self._port - ) - - try: - _LOGGER.debug("Retrieving profiles") - - media_service = self._camera.create_media_service() - - profiles = await media_service.GetProfiles() - - _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) - - if self._profile_index >= len(profiles): - _LOGGER.warning( - "ONVIF Camera '%s' doesn't provide profile %d." - " Using the last profile.", - self._name, - self._profile_index, - ) - self._profile_index = -1 - - _LOGGER.debug("Using profile index '%d'", self._profile_index) - - _LOGGER.debug("Retrieving stream uri") - - # Fix Onvif setup error on Goke GK7102 based IP camera - # where we need to recreate media_service #26781 - media_service = self._camera.create_media_service() - - req = media_service.create_type("GetStreamUri") - req.ProfileToken = profiles[self._profile_index].token - req.StreamSetup = { - "Stream": "RTP-Unicast", - "Transport": {"Protocol": "RTSP"}, - } - - stream_uri = await media_service.GetStreamUri(req) - uri_no_auth = stream_uri.Uri - uri_for_log = uri_no_auth.replace("rtsp://", "rtsp://:@", 1) - self._input = uri_no_auth.replace( - "rtsp://", f"rtsp://{self._username}:{self._password}@", 1 - ) - - _LOGGER.debug( - "ONVIF Camera Using the following URL for %s: %s", - self._name, - uri_for_log, - ) - except exceptions.ONVIFError as err: - _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) - - async def async_obtain_snapshot_uri(self): - """Set the snapshot uri for the camera.""" - _LOGGER.debug( - "Connecting with ONVIF Camera: %s on port %s", self._host, self._port - ) - - try: - _LOGGER.debug("Retrieving profiles") - - media_service = self._camera.create_media_service() - - profiles = await media_service.GetProfiles() - - _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) - - if self._profile_index >= len(profiles): - _LOGGER.warning( - "ONVIF Camera '%s' doesn't provide profile %d." - " Using the last profile.", - self._name, - self._profile_index, - ) - self._profile_index = -1 - - _LOGGER.debug("Using profile index '%d'", self._profile_index) - - _LOGGER.debug("Retrieving snapshot uri") - - # Fix Onvif setup error on Goke GK7102 based IP camera - # where we need to recreate media_service #26781 - media_service = self._camera.create_media_service() - - req = media_service.create_type("GetSnapshotUri") - req.ProfileToken = profiles[self._profile_index].token - - try: - snapshot_uri = await media_service.GetSnapshotUri(req) - self._snapshot = snapshot_uri.Uri - except ServerDisconnectedError as err: - _LOGGER.debug("Camera does not support GetSnapshotUri: %s", err) - - _LOGGER.debug( - "ONVIF Camera Using the following URL for %s snapshot: %s", - self._name, - self._snapshot, - ) - except exceptions.ONVIFError as err: - _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) - - def setup_ptz(self): - """Set up PTZ if available.""" - _LOGGER.debug("Setting up the ONVIF PTZ service") - if self._camera.get_service("ptz", create=False) is None: - _LOGGER.debug("PTZ is not available") - else: - self._ptz_service = self._camera.create_ptz_service() - _LOGGER.debug("Completed set up of the ONVIF camera component") - - async def async_perform_ptz( - self, pan, tilt, zoom, distance, speed, move_mode, continuous_duration - ): - """Perform a PTZ action on the camera.""" - if self._ptz_service is None: - _LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name) - return - - if self._ptz_service: - pan_val = distance * PAN_FACTOR.get(pan, 0) - tilt_val = distance * TILT_FACTOR.get(tilt, 0) - zoom_val = distance * ZOOM_FACTOR.get(zoom, 0) - speed_val = speed - _LOGGER.debug( - "Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f", - move_mode, - pan_val, - tilt_val, - zoom_val, - speed_val, - ) - try: - req = self._ptz_service.create_type(move_mode) - req.ProfileToken = await self.async_obtain_profile_token() - if move_mode == CONTINUOUS_MOVE: - req.Velocity = { - "PanTilt": {"x": pan_val, "y": tilt_val}, - "Zoom": {"x": zoom_val}, - } - - await self._ptz_service.ContinuousMove(req) - await asyncio.sleep(continuous_duration) - req = self._ptz_service.create_type("Stop") - req.ProfileToken = await self.async_obtain_profile_token() - await self._ptz_service.Stop({"ProfileToken": req.ProfileToken}) - elif move_mode == RELATIVE_MOVE: - req.Translation = { - "PanTilt": {"x": pan_val, "y": tilt_val}, - "Zoom": {"x": zoom_val}, - } - req.Speed = { - "PanTilt": {"x": speed_val, "y": speed_val}, - "Zoom": {"x": speed_val}, - } - await self._ptz_service.RelativeMove(req) - elif move_mode == ABSOLUTE_MOVE: - req.Position = { - "PanTilt": {"x": pan_val, "y": tilt_val}, - "Zoom": {"x": zoom_val}, - } - req.Speed = { - "PanTilt": {"x": speed_val, "y": speed_val}, - "Zoom": {"x": speed_val}, - } - await self._ptz_service.AbsoluteMove(req) - except exceptions.ONVIFError as err: - if "Bad Request" in err.reason: - self._ptz_service = None - _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) - else: - _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) - - async def async_added_to_hass(self): - """Handle entity addition to hass.""" - _LOGGER.debug("Camera '%s' added to hass", self._name) - - if ONVIF_DATA not in self.hass.data: - self.hass.data[ONVIF_DATA] = {} - self.hass.data[ONVIF_DATA][ENTITIES] = [] - self.hass.data[ONVIF_DATA][ENTITIES].append(self) + return self._stream_uri async def async_camera_image(self): """Return a still image response from the camera.""" - _LOGGER.debug("Retrieving image from camera '%s'", self._name) image = None - if self._snapshot is not None: + if self.device.capabilities.snapshot: + if self._snapshot_uri is None: + self._snapshot_uri = await self.device.async_get_snapshot_uri( + self.profile + ) + auth = None - if self._username and self._password: - auth = HTTPDigestAuth(self._username, self._password) + if self.device.username and self.device.password: + auth = HTTPDigestAuth(self.device.username, self.device.password) def fetch(): """Read image from a URL.""" try: - response = requests.get(self._snapshot, timeout=5, auth=auth) + response = requests.get(self._snapshot_uri, timeout=5, auth=auth) if response.status_code < 300: return response.content except requests.exceptions.RequestException as error: - _LOGGER.error( + LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", - self._name, + self.device.name, error, ) return None - image = await self.hass.async_add_job(fetch) + image = await self.hass.async_add_executor_job(fetch) if image is None: - # Don't keep trying the snapshot URL - self._snapshot = None - ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) image = await asyncio.shield( ffmpeg.get_image( - self._input, + self._stream_uri, output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments, + extra_cmd=self.device.config_entry.options.get( + CONF_EXTRA_ARGUMENTS + ), ) ) @@ -549,12 +160,15 @@ class ONVIFHassCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - _LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) + LOGGER.debug("Handling mjpeg stream from camera '%s'", self.device.name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop) - await stream.open_camera(self._input, extra_cmd=self._ffmpeg_arguments) + await stream.open_camera( + self._stream_uri, + extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), + ) try: stream_reader = await stream.get_reader() @@ -567,25 +181,26 @@ class ONVIFHassCamera(Camera): finally: await stream.close() - @property - def supported_features(self): - """Return supported features.""" - if self._input: - return SUPPORT_STREAM - return 0 - - async def stream_source(self): - """Return the stream source.""" - return self._input - - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - if self._profile_index: - return f"{self._mac}_{self._profile_index}" - return self._mac + async def async_perform_ptz( + self, + distance, + speed, + move_mode, + continuous_duration, + preset, + pan=None, + tilt=None, + zoom=None, + ) -> None: + """Perform a PTZ action on the camera.""" + await self.device.async_perform_ptz( + self.profile, + distance, + speed, + move_mode, + continuous_duration, + preset, + pan, + tilt, + zoom, + ) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py new file mode 100644 index 00000000000..ceb861fc7dd --- /dev/null +++ b/homeassistant/components/onvif/config_flow.py @@ -0,0 +1,288 @@ +"""Config flow for ONVIF.""" +from pprint import pformat +from typing import List +from urllib.parse import urlparse + +from onvif.exceptions import ONVIFError +import voluptuous as vol +from wsdiscovery.discovery import ThreadedWSDiscovery as WSDiscovery +from wsdiscovery.scope import Scope +from wsdiscovery.service import Service +from zeep.exceptions import Fault + +from homeassistant import config_entries +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import callback + +# pylint: disable=unused-import +from .const import ( + CONF_DEVICE_ID, + CONF_RTSP_TRANSPORT, + DEFAULT_ARGUMENTS, + DEFAULT_PORT, + DOMAIN, + LOGGER, + RTSP_TRANS_PROTOCOLS, +) +from .device import get_device + +CONF_MANUAL_INPUT = "Manually configure ONVIF device" + + +def wsdiscovery() -> List[Service]: + """Get ONVIF Profile S devices from network.""" + discovery = WSDiscovery(ttl=4) + discovery.start() + services = discovery.searchServices( + scopes=[Scope("onvif://www.onvif.org/Profile/Streaming")] + ) + discovery.stop() + return services + + +async def async_discovery(hass) -> bool: + """Return if there are devices that can be discovered.""" + LOGGER.debug("Starting ONVIF discovery...") + services = await hass.async_add_executor_job(wsdiscovery) + + devices = [] + for service in services: + url = urlparse(service.getXAddrs()[0]) + device = { + CONF_DEVICE_ID: None, + CONF_NAME: service.getEPR(), + CONF_HOST: url.hostname, + CONF_PORT: url.port or 80, + } + for scope in service.getScopes(): + scope_str = scope.getValue() + if scope_str.lower().startswith("onvif://www.onvif.org/name"): + device[CONF_NAME] = scope_str.split("/")[-1] + if scope_str.lower().startswith("onvif://www.onvif.org/mac"): + device[CONF_DEVICE_ID] = scope_str.split("/")[-1] + devices.append(device) + + return devices + + +class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a ONVIF config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OnvifOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the ONVIF config flow.""" + self.device_id = None + self.devices = [] + self.onvif_config = {} + + async def async_step_user(self, user_input=None): + """Handle user flow.""" + if user_input is not None: + return await self.async_step_device() + + return self.async_show_form(step_id="user") + + async def async_step_device(self, user_input=None): + """Handle WS-Discovery. + + Let user choose between discovered devices and manual configuration. + If no device is found allow user to manually input configuration. + """ + if user_input: + + if CONF_MANUAL_INPUT == user_input[CONF_HOST]: + return await self.async_step_manual_input() + + for device in self.devices: + name = f"{device[CONF_NAME]} ({device[CONF_HOST]})" + if name == user_input[CONF_HOST]: + self.device_id = device[CONF_DEVICE_ID] + self.onvif_config = { + CONF_NAME: device[CONF_NAME], + CONF_HOST: device[CONF_HOST], + CONF_PORT: device[CONF_PORT], + } + return await self.async_step_auth() + + discovery = await async_discovery(self.hass) + for device in discovery: + configured = any( + entry.unique_id == device[CONF_DEVICE_ID] + for entry in self._async_current_entries() + ) + + if not configured: + self.devices.append(device) + + LOGGER.debug("Discovered ONVIF devices %s", pformat(self.devices)) + + if self.devices: + names = [ + f"{device[CONF_NAME]} ({device[CONF_HOST]})" for device in self.devices + ] + + names.append(CONF_MANUAL_INPUT) + + return self.async_show_form( + step_id="device", + data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(names)}), + ) + + return await self.async_step_manual_input() + + async def async_step_manual_input(self, user_input=None): + """Manual configuration.""" + if user_input: + self.onvif_config = user_input + return await self.async_step_auth() + + return self.async_show_form( + step_id="manual_input", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + ) + + async def async_step_auth(self, user_input=None): + """Username and Password configuration for ONVIF device.""" + if user_input: + self.onvif_config[CONF_USERNAME] = user_input[CONF_USERNAME] + self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD] + return await self.async_step_profiles() + + return self.async_show_form( + step_id="auth", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + ) + + async def async_step_profiles(self, user_input=None): + """Fetch ONVIF device profiles.""" + errors = {} + + LOGGER.debug( + "Fetching profiles from ONVIF device %s", pformat(self.onvif_config) + ) + + device = get_device( + self.hass, + self.onvif_config[CONF_HOST], + self.onvif_config[CONF_PORT], + self.onvif_config[CONF_USERNAME], + self.onvif_config[CONF_PASSWORD], + ) + + await device.update_xaddrs() + + try: + # Get the MAC address to use as the unique ID for the config flow + if not self.device_id: + devicemgmt = device.create_devicemgmt_service() + network_interfaces = await devicemgmt.GetNetworkInterfaces() + for interface in network_interfaces: + if interface.Enabled: + self.device_id = interface.Info.HwAddress + + if self.device_id is None: + return self.async_abort(reason="no_mac") + + await self.async_set_unique_id(self.device_id, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self.onvif_config[CONF_HOST], + CONF_PORT: self.onvif_config[CONF_PORT], + CONF_NAME: self.onvif_config[CONF_NAME], + } + ) + + # Verify there is an H264 profile + media_service = device.create_media_service() + profiles = await media_service.GetProfiles() + h264 = any( + profile.VideoEncoderConfiguration.Encoding == "H264" + for profile in profiles + ) + + if not h264: + return self.async_abort(reason="no_h264") + + title = f"{self.onvif_config[CONF_NAME]} - {self.device_id}" + return self.async_create_entry(title=title, data=self.onvif_config) + + except ONVIFError as err: + LOGGER.error( + "Couldn't setup ONVIF device '%s'. Error: %s", + self.onvif_config[CONF_NAME], + err, + ) + return self.async_abort(reason="onvif_error") + + except Fault: + errors["base"] = "connection_failed" + + return self.async_show_form(step_id="auth", errors=errors) + + async def async_step_import(self, user_input): + """Handle import.""" + self.onvif_config = user_input + return await self.async_step_profiles() + + +class OnvifOptionsFlowHandler(config_entries.OptionsFlow): + """Handle ONVIF options.""" + + def __init__(self, config_entry): + """Initialize ONVIF options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the ONVIF options.""" + return await self.async_step_onvif_devices() + + async def async_step_onvif_devices(self, user_input=None): + """Manage the ONVIF devices options.""" + if user_input is not None: + self.options[CONF_EXTRA_ARGUMENTS] = user_input[CONF_EXTRA_ARGUMENTS] + self.options[CONF_RTSP_TRANSPORT] = user_input[CONF_RTSP_TRANSPORT] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="onvif_devices", + data_schema=vol.Schema( + { + vol.Optional( + CONF_EXTRA_ARGUMENTS, + default=self.config_entry.options.get( + CONF_EXTRA_ARGUMENTS, DEFAULT_ARGUMENTS + ), + ): str, + vol.Optional( + CONF_RTSP_TRANSPORT, + default=self.config_entry.options.get( + CONF_RTSP_TRANSPORT, RTSP_TRANS_PROTOCOLS[0] + ), + ): vol.In(RTSP_TRANS_PROTOCOLS), + } + ), + ) diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py new file mode 100644 index 00000000000..ddc1cc22801 --- /dev/null +++ b/homeassistant/components/onvif/const.py @@ -0,0 +1,42 @@ +"""Constants for the onvif component.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "onvif" + +DEFAULT_NAME = "ONVIF Camera" +DEFAULT_PORT = 5000 +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "888888" +DEFAULT_ARGUMENTS = "-pred 1" + +CONF_DEVICE_ID = "deviceid" +CONF_RTSP_TRANSPORT = "rtsp_transport" + +RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] + +ATTR_PAN = "pan" +ATTR_TILT = "tilt" +ATTR_ZOOM = "zoom" +ATTR_DISTANCE = "distance" +ATTR_SPEED = "speed" +ATTR_MOVE_MODE = "move_mode" +ATTR_CONTINUOUS_DURATION = "continuous_duration" +ATTR_PRESET = "preset" + +DIR_UP = "UP" +DIR_DOWN = "DOWN" +DIR_LEFT = "LEFT" +DIR_RIGHT = "RIGHT" +ZOOM_OUT = "ZOOM_OUT" +ZOOM_IN = "ZOOM_IN" +PAN_FACTOR = {DIR_RIGHT: 1, DIR_LEFT: -1} +TILT_FACTOR = {DIR_UP: 1, DIR_DOWN: -1} +ZOOM_FACTOR = {ZOOM_IN: 1, ZOOM_OUT: -1} +CONTINUOUS_MOVE = "ContinuousMove" +RELATIVE_MOVE = "RelativeMove" +ABSOLUTE_MOVE = "AbsoluteMove" +GOTOPRESET_MOVE = "GotoPreset" + +SERVICE_PTZ = "ptz" diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py new file mode 100644 index 00000000000..8e69e148da3 --- /dev/null +++ b/homeassistant/components/onvif/device.py @@ -0,0 +1,435 @@ +"""ONVIF device abstraction.""" +import asyncio +import datetime as dt +import os +from typing import List + +from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError +import onvif +from onvif import ONVIFCamera +from onvif.exceptions import ONVIFError +from zeep.asyncio import AsyncTransport +from zeep.exceptions import Fault + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.util.dt as dt_util + +from .const import ( + ABSOLUTE_MOVE, + CONTINUOUS_MOVE, + GOTOPRESET_MOVE, + LOGGER, + PAN_FACTOR, + RELATIVE_MOVE, + TILT_FACTOR, + ZOOM_FACTOR, +) +from .event import EventManager +from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video + + +class ONVIFDevice: + """Manages an ONVIF device.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None): + """Initialize the device.""" + self.hass: HomeAssistant = hass + self.config_entry: ConfigEntry = config_entry + self.available: bool = True + + self.device: ONVIFCamera = None + self.events: EventManager = None + + self.info: DeviceInfo = DeviceInfo() + self.capabilities: Capabilities = Capabilities() + self.profiles: List[Profile] = [] + self.max_resolution: int = 0 + + self._dt_diff_seconds: int = 0 + + @property + def name(self) -> str: + """Return the name of this device.""" + return self.config_entry.data[CONF_NAME] + + @property + def host(self) -> str: + """Return the host of this device.""" + return self.config_entry.data[CONF_HOST] + + @property + def port(self) -> int: + """Return the port of this device.""" + return self.config_entry.data[CONF_PORT] + + @property + def username(self) -> int: + """Return the username of this device.""" + return self.config_entry.data[CONF_USERNAME] + + @property + def password(self) -> int: + """Return the password of this device.""" + return self.config_entry.data[CONF_PASSWORD] + + async def async_setup(self) -> bool: + """Set up the device.""" + self.device = get_device( + self.hass, + host=self.config_entry.data[CONF_HOST], + port=self.config_entry.data[CONF_PORT], + username=self.config_entry.data[CONF_USERNAME], + password=self.config_entry.data[CONF_PASSWORD], + ) + + # Get all device info + try: + await self.device.update_xaddrs() + await self.async_check_date_and_time() + self.info = await self.async_get_device_info() + self.capabilities = await self.async_get_capabilities() + self.profiles = await self.async_get_profiles() + + if self.capabilities.ptz: + self.device.create_ptz_service() + + if self._dt_diff_seconds > 300 and self.capabilities.events: + self.capabilities.events = False + LOGGER.warning( + "The system clock on '%s' is more than 5 minutes off. " + "Although this device supports events, they will be " + "disabled until the device clock is fixed as we will " + "not be able to renew the subscription.", + self.name, + ) + + if self.capabilities.events: + self.events = EventManager( + self.hass, self.device, self.config_entry.unique_id + ) + + # Determine max resolution from profiles + self.max_resolution = max( + profile.video.resolution.width + for profile in self.profiles + if profile.video.encoding == "H264" + ) + except ClientConnectionError as err: + LOGGER.warning( + "Couldn't connect to camera '%s', but will retry later. Error: %s", + self.name, + err, + ) + self.available = False + except Fault as err: + LOGGER.error( + "Couldn't connect to camera '%s', please verify " + "that the credentials are correct. Error: %s", + self.name, + err, + ) + return False + + return True + + async def async_check_date_and_time(self) -> None: + """Warns if device and system date not synced.""" + LOGGER.debug("Setting up the ONVIF device management service") + devicemgmt = self.device.create_devicemgmt_service() + + LOGGER.debug("Retrieving current device date/time") + try: + system_date = dt_util.utcnow() + device_time = await devicemgmt.GetSystemDateAndTime() + if not device_time: + LOGGER.debug( + """Couldn't get device '%s' date/time. + GetSystemDateAndTime() return null/empty""", + self.name, + ) + return + + if device_time.UTCDateTime: + tzone = dt_util.UTC + cdate = device_time.UTCDateTime + else: + tzone = ( + dt_util.get_time_zone(device_time.TimeZone) + or dt_util.DEFAULT_TIME_ZONE + ) + cdate = device_time.LocalDateTime + + if cdate is None: + LOGGER.warning("Could not retrieve date/time on this camera") + else: + cam_date = dt.datetime( + cdate.Date.Year, + cdate.Date.Month, + cdate.Date.Day, + cdate.Time.Hour, + cdate.Time.Minute, + cdate.Time.Second, + 0, + tzone, + ) + + cam_date_utc = cam_date.astimezone(dt_util.UTC) + + LOGGER.debug( + "Device date/time: %s | System date/time: %s", + cam_date_utc, + system_date, + ) + + dt_diff = cam_date - system_date + self._dt_diff_seconds = dt_diff.total_seconds() + + if self._dt_diff_seconds > 5: + LOGGER.warning( + "The date/time on the device (UTC) is '%s', " + "which is different from the system '%s', " + "this could lead to authentication issues", + cam_date_utc, + system_date, + ) + except ServerDisconnectedError as err: + LOGGER.warning( + "Couldn't get device '%s' date/time. Error: %s", self.name, err + ) + + async def async_get_device_info(self) -> DeviceInfo: + """Obtain information about this device.""" + devicemgmt = self.device.create_devicemgmt_service() + device_info = await devicemgmt.GetDeviceInformation() + return DeviceInfo( + device_info.Manufacturer, + device_info.Model, + device_info.FirmwareVersion, + self.config_entry.unique_id, + ) + + async def async_get_capabilities(self): + """Obtain information about the available services on the device.""" + snapshot = False + try: + media_service = self.device.create_media_service() + media_capabilities = await media_service.GetServiceCapabilities() + snapshot = media_capabilities.SnapshotUri + except (ONVIFError, Fault): + pass + + pullpoint = False + try: + event_service = self.device.create_events_service() + event_capabilities = await event_service.GetServiceCapabilities() + pullpoint = event_capabilities.WSPullPointSupport + except (ONVIFError, Fault): + pass + + ptz = False + try: + self.device.get_definition("ptz") + ptz = True + except ONVIFError: + pass + + return Capabilities(snapshot, pullpoint, ptz) + + async def async_get_profiles(self) -> List[Profile]: + """Obtain media profiles for this device.""" + media_service = self.device.create_media_service() + result = await media_service.GetProfiles() + profiles = [] + for key, onvif_profile in enumerate(result): + # Only add H264 profiles + if onvif_profile.VideoEncoderConfiguration.Encoding != "H264": + continue + + profile = Profile( + key, + onvif_profile.token, + onvif_profile.Name, + Video( + onvif_profile.VideoEncoderConfiguration.Encoding, + Resolution( + onvif_profile.VideoEncoderConfiguration.Resolution.Width, + onvif_profile.VideoEncoderConfiguration.Resolution.Height, + ), + ), + ) + + # Configure PTZ options + if onvif_profile.PTZConfiguration: + profile.ptz = PTZ( + onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace + is not None, + onvif_profile.PTZConfiguration.DefaultRelativePanTiltTranslationSpace + is not None, + onvif_profile.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace + is not None, + ) + + ptz_service = self.device.get_service("ptz") + presets = await ptz_service.GetPresets(profile.token) + profile.ptz.presets = [preset.token for preset in presets] + + profiles.append(profile) + + return profiles + + async def async_get_snapshot_uri(self, profile: Profile) -> str: + """Get the snapshot URI for a specified profile.""" + if not self.capabilities.snapshot: + return None + + media_service = self.device.create_media_service() + req = media_service.create_type("GetSnapshotUri") + req.ProfileToken = profile.token + result = await media_service.GetSnapshotUri(req) + return result.Uri + + async def async_get_stream_uri(self, profile: Profile) -> str: + """Get the stream URI for a specified profile.""" + media_service = self.device.create_media_service() + req = media_service.create_type("GetStreamUri") + req.ProfileToken = profile.token + req.StreamSetup = { + "Stream": "RTP-Unicast", + "Transport": {"Protocol": "RTSP"}, + } + result = await media_service.GetStreamUri(req) + return result.Uri + + async def async_perform_ptz( + self, + profile: Profile, + distance, + speed, + move_mode, + continuous_duration, + preset, + pan=None, + tilt=None, + zoom=None, + ): + """Perform a PTZ action on the camera.""" + if not self.capabilities.ptz: + LOGGER.warning("PTZ actions are not supported on device '%s'", self.name) + return + + ptz_service = self.device.get_service("ptz") + + pan_val = distance * PAN_FACTOR.get(pan, 0) + tilt_val = distance * TILT_FACTOR.get(tilt, 0) + zoom_val = distance * ZOOM_FACTOR.get(zoom, 0) + speed_val = speed + preset_val = preset + LOGGER.debug( + "Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f | Preset = %s", + move_mode, + pan_val, + tilt_val, + zoom_val, + speed_val, + preset_val, + ) + try: + req = ptz_service.create_type(move_mode) + req.ProfileToken = profile.token + if move_mode == CONTINUOUS_MOVE: + # Guard against unsupported operation + if not profile.ptz.continuous: + LOGGER.warning( + "ContinuousMove not supported on device '%s'", self.name + ) + return + + req.Velocity = { + "PanTilt": {"x": pan_val, "y": tilt_val}, + "Zoom": {"x": zoom_val}, + } + + await ptz_service.ContinuousMove(req) + await asyncio.sleep(continuous_duration) + req = ptz_service.create_type("Stop") + req.ProfileToken = profile.token + await ptz_service.Stop({"ProfileToken": req.ProfileToken}) + elif move_mode == RELATIVE_MOVE: + # Guard against unsupported operation + if not profile.ptz.relative: + LOGGER.warning( + "ContinuousMove not supported on device '%s'", self.name + ) + return + + req.Translation = { + "PanTilt": {"x": pan_val, "y": tilt_val}, + "Zoom": {"x": zoom_val}, + } + req.Speed = { + "PanTilt": {"x": speed_val, "y": speed_val}, + "Zoom": {"x": speed_val}, + } + await ptz_service.RelativeMove(req) + elif move_mode == ABSOLUTE_MOVE: + # Guard against unsupported operation + if not profile.ptz.absolute: + LOGGER.warning( + "ContinuousMove not supported on device '%s'", self.name + ) + return + + req.Position = { + "PanTilt": {"x": pan_val, "y": tilt_val}, + "Zoom": {"x": zoom_val}, + } + req.Speed = { + "PanTilt": {"x": speed_val, "y": speed_val}, + "Zoom": {"x": speed_val}, + } + await ptz_service.AbsoluteMove(req) + elif move_mode == GOTOPRESET_MOVE: + # Guard against unsupported operation + if preset_val not in profile.ptz.presets: + LOGGER.warning( + "PTZ preset '%s' does not exist on device '%s'. Available Presets: %s", + preset_val, + self.name, + profile.ptz.presets.join(", "), + ) + return + + req.PresetToken = preset_val + req.Speed = { + "PanTilt": {"x": speed_val, "y": speed_val}, + "Zoom": {"x": speed_val}, + } + await ptz_service.GotoPreset(req) + except ONVIFError as err: + if "Bad Request" in err.reason: + LOGGER.warning("Device '%s' doesn't support PTZ.", self.name) + else: + LOGGER.error("Error trying to perform PTZ action: %s", err) + + +def get_device(hass, host, port, username, password) -> ONVIFCamera: + """Get ONVIFCamera instance.""" + session = async_get_clientsession(hass) + transport = AsyncTransport(None, session=session) + return ONVIFCamera( + host, + port, + username, + password, + f"{os.path.dirname(onvif.__file__)}/wsdl/", + transport=transport, + ) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py new file mode 100644 index 00000000000..888fe5bd92b --- /dev/null +++ b/homeassistant/components/onvif/event.py @@ -0,0 +1,173 @@ +"""ONVIF event abstraction.""" +import datetime as dt +from typing import Callable, Dict, List, Optional, Set + +from aiohttp.client_exceptions import ServerDisconnectedError +from onvif import ONVIFCamera, ONVIFService +from zeep.exceptions import Fault + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util import dt as dt_util + +from .const import LOGGER +from .models import Event +from .parsers import PARSERS + +UNHANDLED_TOPICS = set() + + +class EventManager: + """ONVIF Event Manager.""" + + def __init__(self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str): + """Initialize event manager.""" + self.hass: HomeAssistant = hass + self.device: ONVIFCamera = device + self.unique_id: str = unique_id + self.started: bool = False + + self._subscription: ONVIFService = None + self._events: Dict[str, Event] = {} + self._listeners: List[CALLBACK_TYPE] = [] + self._unsub_refresh: Optional[CALLBACK_TYPE] = None + + super().__init__() + + @property + def platforms(self) -> Set[str]: + """Return platforms to setup.""" + return {event.platform for event in self._events.values()} + + @callback + def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]: + """Listen for data updates.""" + # This is the first listener, set up polling. + if not self._listeners: + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, + self.async_pull_messages, + dt_util.utcnow() + dt.timedelta(seconds=1), + ) + + self._listeners.append(update_callback) + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self.async_remove_listener(update_callback) + + return remove_listener + + @callback + def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Remove data update.""" + self._listeners.remove(update_callback) + + if not self._listeners and self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + async def async_start(self) -> bool: + """Start polling events.""" + if await self.device.create_pullpoint_subscription(): + # Initialize events + pullpoint = self.device.create_pullpoint_service() + await pullpoint.SetSynchronizationPoint() + req = pullpoint.create_type("PullMessages") + req.MessageLimit = 100 + req.Timeout = dt.timedelta(seconds=5) + response = await pullpoint.PullMessages(req) + + # Parse event initialization + await self.async_parse_messages(response.NotificationMessage) + + # Create subscription manager + self._subscription = self.device.create_subscription_service( + "PullPointSubscription" + ) + + self.started = True + + return self.started + + async def async_stop(self, event=None) -> None: + """Unsubscribe from events.""" + if not self._subscription: + return + + await self._subscription.Unsubscribe() + self._subscription = None + + async def async_renew(self) -> None: + """Renew subscription.""" + if not self._subscription: + return + + termination_time = (dt_util.utcnow() + dt.timedelta(minutes=30)).isoformat() + await self._subscription.Renew(termination_time) + + async def async_pull_messages(self, _now: dt = None) -> None: + """Pull messages from device.""" + try: + pullpoint = self.device.get_service("pullpoint") + req = pullpoint.create_type("PullMessages") + req.MessageLimit = 100 + req.Timeout = dt.timedelta(seconds=60) + response = await pullpoint.PullMessages(req) + + # Renew subscription if less than 60 seconds left + if (response.TerminationTime - dt_util.utcnow()).total_seconds() < 60: + await self.async_renew() + + # Parse response + await self.async_parse_messages(response.NotificationMessage) + + except ServerDisconnectedError: + pass + except Fault: + pass + + # Update entities + for update_callback in self._listeners: + update_callback() + + # Reschedule another pull + if self._listeners: + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, + self.async_pull_messages, + dt_util.utcnow() + dt.timedelta(seconds=1), + ) + + # pylint: disable=protected-access + async def async_parse_messages(self, messages) -> None: + """Parse notification message.""" + for msg in messages: + topic = msg.Topic._value_1 + parser = PARSERS.get(topic) + if not parser: + if topic not in UNHANDLED_TOPICS: + LOGGER.info( + "No registered handler for event from %s: %s", + self.unique_id, + msg, + ) + UNHANDLED_TOPICS.add(topic) + continue + + event = await parser(self.unique_id, msg) + + if not event: + LOGGER.warning("Unable to parse event from %s: %s", self.unique_id, msg) + return + + self._events[event.uid] = event + + def get_uid(self, uid) -> Event: + """Retrieve event for given id.""" + return self._events[uid] + + def get_platform(self, platform) -> List[Event]: + """Retrieve events for given platform.""" + return [event for event in self._events.values() if event.platform == platform] diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index a927fd9072b..f291f9c6613 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,8 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==0.2.0"], + "requirements": ["onvif-zeep-async==0.3.0", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": ["@hunterjm"], + "config_flow": true } diff --git a/homeassistant/components/onvif/models.py b/homeassistant/components/onvif/models.py new file mode 100644 index 00000000000..686d9fecbda --- /dev/null +++ b/homeassistant/components/onvif/models.py @@ -0,0 +1,72 @@ +"""ONVIF models.""" +from dataclasses import dataclass +from typing import Any, List + + +@dataclass +class DeviceInfo: + """Represent device information.""" + + manufacturer: str = None + model: str = None + fw_version: str = None + mac: str = None + + +@dataclass +class Resolution: + """Represent video resolution.""" + + width: int + height: int + + +@dataclass +class Video: + """Represent video encoding settings.""" + + encoding: str + resolution: Resolution + + +@dataclass +class PTZ: + """Represents PTZ configuration on a profile.""" + + continuous: bool + relative: bool + absolute: bool + presets: List[str] = None + + +@dataclass +class Profile: + """Represent a ONVIF Profile.""" + + index: int + token: str + name: str + video: Video + ptz: PTZ = None + + +@dataclass +class Capabilities: + """Represents Service capabilities.""" + + snapshot: bool = False + events: bool = False + ptz: bool = False + + +@dataclass +class Event: + """Represents a ONVIF event.""" + + uid: str + name: str + platform: str + device_class: str = None + unit_of_measurement: str = None + value: Any = None + entity_enabled: bool = True diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py new file mode 100644 index 00000000000..438601106b5 --- /dev/null +++ b/homeassistant/components/onvif/parsers.py @@ -0,0 +1,358 @@ +"""ONVIF event parsers.""" +from homeassistant.util import dt as dt_util +from homeassistant.util.decorator import Registry + +from .models import Event + +PARSERS = Registry() + + +@PARSERS.register("tns1:VideoSource/MotionAlarm") +# pylint: disable=protected-access +async def async_parse_motion_alarm(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:VideoSource/MotionAlarm + """ + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + f"{source} Motion Alarm", + "binary_sensor", + "motion", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:VideoSource/ImageTooBlurry/AnalyticsService") +@PARSERS.register("tns1:VideoSource/ImageTooBlurry/ImagingService") +@PARSERS.register("tns1:VideoSource/ImageTooBlurry/RecordingService") +# pylint: disable=protected-access +async def async_parse_image_too_blurry(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:VideoSource/ImageTooBlurry/* + """ + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + f"{source} Image Too Blurry", + "binary_sensor", + "problem", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:VideoSource/ImageTooDark/AnalyticsService") +@PARSERS.register("tns1:VideoSource/ImageTooDark/ImagingService") +@PARSERS.register("tns1:VideoSource/ImageTooDark/RecordingService") +# pylint: disable=protected-access +async def async_parse_image_too_dark(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:VideoSource/ImageTooDark/* + """ + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + f"{source} Image Too Dark", + "binary_sensor", + "problem", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:VideoSource/ImageTooBright/AnalyticsService") +@PARSERS.register("tns1:VideoSource/ImageTooBright/ImagingService") +@PARSERS.register("tns1:VideoSource/ImageTooBright/RecordingService") +# pylint: disable=protected-access +async def async_parse_image_too_bright(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:VideoSource/ImageTooBright/* + """ + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + f"{source} Image Too Bright", + "binary_sensor", + "problem", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:VideoSource/GlobalSceneChange/AnalyticsService") +@PARSERS.register("tns1:VideoSource/GlobalSceneChange/ImagingService") +@PARSERS.register("tns1:VideoSource/GlobalSceneChange/RecordingService") +# pylint: disable=protected-access +async def async_parse_scene_change(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:VideoSource/GlobalSceneChange/* + """ + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + f"{source} Global Scene Change", + "binary_sensor", + "problem", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:AudioAnalytics/Audio/DetectedSound") +# pylint: disable=protected-access +async def async_parse_detected_sound(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:AudioAnalytics/Audio/DetectedSound + """ + try: + audio_source = "" + audio_analytics = "" + rule = "" + for source in msg.Message._value_1.Source.SimpleItem: + if source.Name == "AudioSourceConfigurationToken": + audio_source = source.Value + if source.Name == "AudioAnalyticsConfigurationToken": + audio_analytics = source.Value + if source.Name == "Rule": + rule = source.Value + + return Event( + f"{uid}_{msg.Topic._value_1}_{audio_source}_{audio_analytics}_{rule}", + f"{rule} Detected Sound", + "binary_sensor", + "sound", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:RuleEngine/FieldDetector/ObjectsInside") +# pylint: disable=protected-access +async def async_parse_field_detector(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:RuleEngine/FieldDetector/ObjectsInside + """ + try: + video_source = "" + video_analytics = "" + rule = "" + for source in msg.Message._value_1.Source.SimpleItem: + if source.Name == "VideoSourceConfigurationToken": + video_source = source.Value + if source.Name == "VideoAnalyticsConfigurationToken": + video_analytics = source.Value + if source.Name == "Rule": + rule = source.Value + + evt = Event( + f"{uid}_{msg.Topic._value_1}_{video_source}_{video_analytics}_{rule}", + f"{rule} Field Detection", + "binary_sensor", + "motion", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + return evt + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:RuleEngine/CellMotionDetector/Motion") +# pylint: disable=protected-access +async def async_parse_cell_motion_detector(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:RuleEngine/CellMotionDetector/Motion + """ + try: + video_source = "" + video_analytics = "" + rule = "" + for source in msg.Message._value_1.Source.SimpleItem: + if source.Name == "VideoSourceConfigurationToken": + video_source = source.Value + if source.Name == "VideoAnalyticsConfigurationToken": + video_analytics = source.Value + if source.Name == "Rule": + rule = source.Value + + return Event( + f"{uid}_{msg.Topic._value_1}_{video_source}_{video_analytics}_{rule}", + f"{rule} Cell Motion Detection", + "binary_sensor", + "motion", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:RuleEngine/TamperDetector/Tamper") +# pylint: disable=protected-access +async def async_parse_tamper_detector(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:RuleEngine/TamperDetector/Tamper + """ + try: + video_source = "" + video_analytics = "" + rule = "" + for source in msg.Message._value_1.Source.SimpleItem: + if source.Name == "VideoSourceConfigurationToken": + video_source = source.Value + if source.Name == "VideoAnalyticsConfigurationToken": + video_analytics = source.Value + if source.Name == "Rule": + rule = source.Value + + return Event( + f"{uid}_{msg.Topic._value_1}_{video_source}_{video_analytics}_{rule}", + f"{rule} Tamper Detection", + "binary_sensor", + "problem", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:Device/HardwareFailure/StorageFailure") +# pylint: disable=protected-access +async def async_parse_storage_failure(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:Device/HardwareFailure/StorageFailure + """ + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + "Storage Failure", + "binary_sensor", + "problem", + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "true", + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:Monitoring/ProcessorUsage") +# pylint: disable=protected-access +async def async_parse_processor_usage(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:Monitoring/ProcessorUsage + """ + try: + usage = float(msg.Message._value_1.Data.SimpleItem[0].Value) + if usage <= 1: + usage *= 100 + + return Event( + f"{uid}_{msg.Topic._value_1}", + "Processor Usage", + "sensor", + None, + "percent", + int(usage), + ) + except (AttributeError, KeyError): + return None + + +@PARSERS.register("tns1:Monitoring/OperatingTime/LastReboot") +# pylint: disable=protected-access +async def async_parse_last_reboot(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:Monitoring/OperatingTime/LastReboot + """ + try: + return Event( + f"{uid}_{msg.Topic._value_1}", + "Last Reboot", + "sensor", + "timestamp", + None, + dt_util.as_local( + dt_util.parse_datetime(msg.Message._value_1.Data.SimpleItem[0].Value) + ), + ) + except (AttributeError, KeyError, ValueError): + return None + + +@PARSERS.register("tns1:Monitoring/OperatingTime/LastReset") +# pylint: disable=protected-access +async def async_parse_last_reset(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:Monitoring/OperatingTime/LastReset + """ + try: + return Event( + f"{uid}_{msg.Topic._value_1}", + "Last Reset", + "sensor", + "timestamp", + None, + dt_util.as_local( + dt_util.parse_datetime(msg.Message._value_1.Data.SimpleItem[0].Value) + ), + entity_enabled=False, + ) + except (AttributeError, KeyError, ValueError): + return None + + +@PARSERS.register("tns1:Monitoring/OperatingTime/LastClockSynchronization") +# pylint: disable=protected-access +async def async_parse_last_clock_sync(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization + """ + try: + return Event( + f"{uid}_{msg.Topic._value_1}", + "Last Clock Synchronization", + "sensor", + "timestamp", + None, + dt_util.as_local( + dt_util.parse_datetime(msg.Message._value_1.Data.SimpleItem[0].Value) + ), + entity_enabled=False, + ) + except (AttributeError, KeyError, ValueError): + return None diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py new file mode 100644 index 00000000000..b1d7ff7986c --- /dev/null +++ b/homeassistant/components/onvif/sensor.py @@ -0,0 +1,87 @@ +"""Support for ONVIF binary sensors.""" +from typing import Optional, Union + +from homeassistant.core import callback + +from .base import ONVIFBaseEntity +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a ONVIF binary sensor.""" + device = hass.data[DOMAIN][config_entry.unique_id] + + entities = { + event.uid: ONVIFSensor(event.uid, device) + for event in device.events.get_platform("sensor") + } + + async_add_entities(entities.values()) + + @callback + def async_check_entities(): + """Check if we have added an entity for the event.""" + new_entities = [] + for event in device.events.get_platform("sensor"): + if event.uid not in entities: + entities[event.uid] = ONVIFSensor(event.uid, device) + new_entities.append(entities[event.uid]) + async_add_entities(new_entities) + + device.events.async_add_listener(async_check_entities) + + return True + + +class ONVIFSensor(ONVIFBaseEntity): + """Representation of a ONVIF sensor event.""" + + def __init__(self, uid, device): + """Initialize the ONVIF binary sensor.""" + self.uid = uid + + super().__init__(device) + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the entity.""" + return self.device.events.get_uid(self.uid).value + + @property + def name(self): + """Return the name of the event.""" + return self.device.events.get_uid(self.uid).name + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self.device.events.get_uid(self.uid).device_class + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + return self.device.events.get_uid(self.uid).unit_of_measurement + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self.uid + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self.device.events.get_uid(self.uid).entity_enabled + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.device.events.async_add_listener(self.async_write_ha_state) + ) diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index 8d14633cc9c..e5a8c9fce35 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -25,7 +25,10 @@ ptz: description: "Set ContinuousMove delay in seconds before stopping the move" default: 0.5 example: 0.5 + preset: + description: "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset" + example: "1" move_mode: - description: "PTZ moving mode. One of ContinuousMove, RelativeMove or AbsoluteMove" + description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove or GotoPreset" default: "RelativeMove" example: "ContinuousMove" diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json new file mode 100644 index 00000000000..b6ae9b98d9a --- /dev/null +++ b/homeassistant/components/onvif/strings.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF device is already configured.", + "already_in_progress": "Config flow for ONVIF device is already in progress.", + "onvif_error": "Error setting up ONVIF device. Check logs for more information.", + "no_h264": "There were no H264 streams available. Check the profile configuration on your device.", + "no_mac": "Could not configure unique ID for ONVIF device." + }, + "error": { + "connection_failed": "Could not connect to ONVIF service with provided credentials." + }, + "step": { + "user": { + "title": "ONVIF device setup", + "description": "By clicking submit, we will search your network for ONVIF devices that support Profile S.\n\nSome manufacturers have started to disable ONVIF by default. Please ensure ONVIF is enabled in your camera's configuration." + }, + "device": { + "data": { + "host": "Select discovered ONVIF device" + }, + "title": "Select ONVIF device" + }, + "manual_input": { + "data": { + "name": "Name", + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "title": "Configure ONVIF device" + }, + "auth": { + "title": "Configure authentication", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "configure_profile": { + "description": "Create camera entity for {profile} at {resolution} resolution?", + "title": "Configure Profiles", + "data": { + "include": "Create camera entity" + } + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG arguments", + "rtsp_transport": "RTSP transport mechanism" + }, + "title": "ONVIF Device Options" + } + } + } +} diff --git a/homeassistant/components/onvif/translations/ca.json b/homeassistant/components/onvif/translations/ca.json new file mode 100644 index 00000000000..a0362abbb8f --- /dev/null +++ b/homeassistant/components/onvif/translations/ca.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositiu ONVIF ja configurat.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ONVIF ja est\u00e0 en curs.", + "no_h264": "No s'han torbat fluxos (streams) H264 disponibles. Comporva la configuraci\u00f3 de perfil en el dispositiu.", + "no_mac": "No s'ha pogut configurar un ID \u00fanic pel dispositiu ONVIF.", + "onvif_error": "Error durant la configuraci\u00f3 del dispositiu ONVIF. Consulta els registres per a m\u00e9s informaci\u00f3." + }, + "error": { + "connection_failed": "No s'ha pogut connectar al servei ONVIF amb les credencials proporcionades." + }, + "step": { + "auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 d'autenticaci\u00f3" + }, + "configure_profile": { + "data": { + "include": "Crea entitat de c\u00e0mera" + }, + "description": "Crear entitat de c\u00e0mera per {profile} amb resoluci\u00f3 {resolution}?", + "title": "Configuraci\u00f3 dels perfils" + }, + "device": { + "data": { + "host": "Selecciona un dispositiu ONVIF descobert" + }, + "title": "Selecci\u00f3 de dispositiu ONVIF" + }, + "manual_input": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "port": "Port" + }, + "title": "Configura el dispositiu ONVIF" + }, + "user": { + "description": "En fer clic a envia, es cercaran a la xarxa dispositius ONVIF que suportin perfils S.\n\nAlguns fabricants han comen\u00e7at a desactivar ONVIF per defecte. Comprova que ONVIF est\u00e0 activat a la configuraci\u00f3 de les c\u00e0meres.", + "title": "Configuraci\u00f3 de dispositiu ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Arguments addicionals FFMPEG", + "rtsp_transport": "Mecanisme de transport RTSP" + }, + "title": "Opcions de dispositiu ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/cs.json b/homeassistant/components/onvif/translations/cs.json new file mode 100644 index 00000000000..dad373fb5e3 --- /dev/null +++ b/homeassistant/components/onvif/translations/cs.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed ONVIF je ji\u017e nakonfigurov\u00e1no", + "no_h264": "Nebyly k dispozici \u017e\u00e1dn\u00e9 H264 streamy. Zkontrolujte konfiguraci profilu v za\u0159\u00edzen\u00ed.", + "no_mac": "Nelze nakonfigurovat jedine\u010dn\u00e9 ID pro za\u0159\u00edzen\u00ed ONVIF.", + "onvif_error": "P\u0159i nastavov\u00e1n\u00ed za\u0159\u00edzen\u00ed ONVIF do\u0161lo k chyb\u011b. Dal\u0161\u00ed informace naleznete v protokolech." + }, + "error": { + "connection_failed": "Nelze se p\u0159ipojit ke slu\u017eb\u011b ONVIF s poskytnut\u00fdmi p\u0159ihla\u0161ovac\u00edmi \u00fadaji." + }, + "step": { + "auth": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Konfigurace ov\u011b\u0159ov\u00e1n\u00ed" + }, + "configure_profile": { + "data": { + "include": "Vytvo\u0159it entitu kamery" + }, + "description": "Chcete vytvo\u0159it entitu kamery pro {profile} s rozli\u0161en\u00edm {resolution}?", + "title": "Nakonfigurovat profily" + }, + "device": { + "data": { + "host": "Vyberte nalezen\u00e1 za\u0159\u00edzen\u00ed ONVIF" + }, + "title": "Vyberte za\u0159\u00edzen\u00ed ONVIF" + }, + "manual_input": { + "data": { + "host": "Adresa za\u0159\u00edzen\u00ed", + "port": "Port" + }, + "title": "Konfigurovat za\u0159\u00edzen\u00ed ONVIF" + }, + "user": { + "description": "Kliknut\u00edm na tla\u010d\u00edtko Odeslat vyhled\u00e1me ve va\u0161\u00ed s\u00edti za\u0159\u00edzen\u00ed ONVIF, kter\u00e1 podporuj\u00ed profil S. \n\nN\u011bkte\u0159\u00ed v\u00fdrobci vypli funkci ONVIF v z\u00e1kladn\u00edm nastaven\u00ed. Ujist\u011bte se, \u017ee je v konfiguraci kamery povolena funkce ONVIF.", + "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Dal\u0161\u00ed FFMPEG argumenty" + }, + "title": "Mo\u017enosti za\u0159\u00edzen\u00ed ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json new file mode 100644 index 00000000000..2a8dcb1b67b --- /dev/null +++ b/homeassistant/components/onvif/translations/de.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Das ONVIF-Ger\u00e4t ist bereits konfiguriert.", + "already_in_progress": "Der Konfigurationsfluss f\u00fcr das ONVIF-Ger\u00e4t wird bereits ausgef\u00fchrt.", + "no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfen Sie die Profilkonfiguration auf Ihrem Ger\u00e4t.", + "no_mac": "Die eindeutige ID f\u00fcr das ONVIF-Ger\u00e4t konnte nicht konfiguriert werden.", + "onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfen Sie die Protokolle auf weitere Informationen." + }, + "error": { + "connection_failed": "Es konnte keine Verbindung zum ONVIF-Dienst mit den angegebenen Anmeldeinformationen hergestellt werden." + }, + "step": { + "auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Konfigurieren Sie die Authentifizierung" + }, + "configure_profile": { + "data": { + "include": "Kameraentit\u00e4t erstellen" + }, + "description": "Kameraentit\u00e4t f\u00fcr {profile} mit {resolution} Aufl\u00f6sung erstellen?", + "title": "Profile konfigurieren" + }, + "device": { + "data": { + "host": "W\u00e4hlen Sie das erkannte ONVIF-Ger\u00e4t aus" + }, + "title": "W\u00e4hlen Sie ONVIF-Ger\u00e4t" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" + }, + "user": { + "description": "Wenn Sie auf Senden klicken, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stellen Sie sicher, dass ONVIF in der Konfiguration Ihrer Kamera aktiviert ist.", + "title": "ONVIF-Ger\u00e4tekonfiguration" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Zus\u00e4tzliche FFMPEG-Argumente", + "rtsp_transport": "RTSP-Transportmechanismus" + }, + "title": "ONVIF-Ger\u00e4teoptionen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json new file mode 100644 index 00000000000..a20b1fcb7e4 --- /dev/null +++ b/homeassistant/components/onvif/translations/en.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF device is already configured.", + "already_in_progress": "Config flow for ONVIF device is already in progress.", + "no_h264": "There were no H264 streams available. Check the profile configuration on your device.", + "no_mac": "Could not configure unique ID for ONVIF device.", + "onvif_error": "Error setting up ONVIF device. Check logs for more information." + }, + "error": { + "connection_failed": "Could not connect to ONVIF service with provided credentials." + }, + "step": { + "auth": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Configure authentication" + }, + "configure_profile": { + "data": { + "include": "Create camera entity" + }, + "description": "Create camera entity for {profile} at {resolution} resolution?", + "title": "Configure Profiles" + }, + "device": { + "data": { + "host": "Select discovered ONVIF device" + }, + "title": "Select ONVIF device" + }, + "manual_input": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + }, + "title": "Configure ONVIF device" + }, + "user": { + "description": "By clicking submit, we will search your network for ONVIF devices that support Profile S.\n\nSome manufacturers have started to disable ONVIF by default. Please ensure ONVIF is enabled in your camera's configuration.", + "title": "ONVIF device setup" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG arguments", + "rtsp_transport": "RTSP transport mechanism" + }, + "title": "ONVIF Device Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/es-419.json b/homeassistant/components/onvif/translations/es-419.json new file mode 100644 index 00000000000..823f1d15880 --- /dev/null +++ b/homeassistant/components/onvif/translations/es-419.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ONVIF ya est\u00e1 configurado.", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en progreso.", + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", + "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", + "onvif_error": "Error al configurar el dispositivo ONVIF. Consulte los registros para obtener m\u00e1s informaci\u00f3n." + }, + "error": { + "connection_failed": "No se pudo conectar al servicio ONVIF con las credenciales proporcionadas." + }, + "step": { + "auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Configurar autenticaci\u00f3n" + }, + "configure_profile": { + "data": { + "include": "Crear entidad de c\u00e1mara" + }, + "description": "\u00bfCrear entidad de c\u00e1mara para {profile} con una {resolution}?", + "title": "Configurar perfiles" + }, + "device": { + "data": { + "host": "Seleccione el dispositivo ONVIF descubierto" + }, + "title": "Seleccionar dispositivo ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar dispositivo ONVIF" + }, + "user": { + "description": "Al hacer clic en enviar, buscaremos en su red dispositivos ONVIF que admitan Profile S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farese de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de su c\u00e1mara.", + "title": "Configuraci\u00f3n del dispositivo ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argumentos adicionales de FFMPEG", + "rtsp_transport": "Mecanismo de transporte RTSP" + }, + "title": "Opciones de dispositivo ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json new file mode 100644 index 00000000000..dd65094838d --- /dev/null +++ b/homeassistant/components/onvif/translations/es.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ONVIF ya est\u00e1 configurado.", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en progreso.", + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", + "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", + "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Revise los registros para m\u00e1s informaci\u00f3n." + }, + "error": { + "connection_failed": "No se pudo conectar al servicio ONVIF con las credenciales proporcionadas." + }, + "step": { + "auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Configurar la autenticaci\u00f3n" + }, + "configure_profile": { + "data": { + "include": "Crear la entidad de la c\u00e1mara" + }, + "description": "\u00bfCrear la entidad de c\u00e1mara para {profile} a {resolution} de resoluci\u00f3n?", + "title": "Configurar los perfiles" + }, + "device": { + "data": { + "host": "Seleccione el dispositivo ONVIF descubierto" + }, + "title": "Seleccione el dispositivo ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "name": "Nombre", + "port": "Puerto" + }, + "title": "Configurar el dispositivo ONVIF" + }, + "user": { + "description": "Al hacer clic en Enviar, buscaremos en su red dispositivos ONVIF compatibles con el perfil S.\n\nAlgunos fabricantes han comenzado a desactivar ONVIF de forma predeterminada. Aseg\u00farese de que ONVIF est\u00e9 activado en la configuraci\u00f3n de la c\u00e1mara.", + "title": "Configuraci\u00f3n del dispositivo ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argumentos extra de FFMPEG", + "rtsp_transport": "Mecanismo de transporte RTSP" + }, + "title": "Opciones del dispositivo ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/fi.json b/homeassistant/components/onvif/translations/fi.json new file mode 100644 index 00000000000..47673eeaaa5 --- /dev/null +++ b/homeassistant/components/onvif/translations/fi.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "title": "M\u00e4\u00e4rit\u00e4 todennus" + }, + "configure_profile": { + "data": { + "include": "Luo kamerakohde" + }, + "title": "M\u00e4\u00e4rit\u00e4 profiilit" + }, + "manual_input": { + "data": { + "host": "Palvelin", + "port": "Portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json new file mode 100644 index 00000000000..b383aad3532 --- /dev/null +++ b/homeassistant/components/onvif/translations/fr.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique ONVIF est d\u00e9j\u00e0 configur\u00e9.", + "already_in_progress": "Le flux de configuration pour le p\u00e9riph\u00e9rique ONVIF est d\u00e9j\u00e0 en cours.", + "no_h264": "Aucun flux H264 n'\u00e9tait disponible. V\u00e9rifiez la configuration du profil sur votre appareil.", + "onvif_error": "Erreur lors de la configuration du p\u00e9riph\u00e9rique ONVIF. Consultez les journaux pour plus d'informations." + }, + "error": { + "connection_failed": "Impossible de se connecter au service ONVIF avec les informations d'identification fournies." + }, + "step": { + "auth": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Configurer l'authentification" + }, + "configure_profile": { + "data": { + "include": "Cr\u00e9er une entit\u00e9 cam\u00e9ra" + }, + "description": "Cr\u00e9er une entit\u00e9 cam\u00e9ra pour {profile} \u00e0 la r\u00e9solution {resolution} ?", + "title": "Configurer les profils" + }, + "device": { + "data": { + "host": "S\u00e9lectionnez le p\u00e9riph\u00e9rique ONVIF d\u00e9couvert" + }, + "title": "S\u00e9lectionnez l'appareil ONVIF" + }, + "manual_input": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "title": "Configurer l\u2019appareil ONVIF" + }, + "user": { + "description": "En cliquant sur soumettre, nous rechercherons sur votre r\u00e9seau, des \u00e9quipements ONVIF qui supporte le Profile S.\n\nCertains constructeurs ont commenc\u00e9 \u00e0 d\u00e9sactiver ONvif par d\u00e9faut. Assurez-vous qu\u2019ONVIF est activ\u00e9 dans la configuration de votre cam\u00e9ra", + "title": "Configuration de l'appareil ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Arguments FFMPEG suppl\u00e9mentaires", + "rtsp_transport": "M\u00e9canisme de transport RTSP" + }, + "title": "Options ONVIF de l'appareil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json new file mode 100644 index 00000000000..a3203bfedb3 --- /dev/null +++ b/homeassistant/components/onvif/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "configure_profile": { + "data": { + "include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4" + } + }, + "manual_input": { + "data": { + "host": "Host", + "port": "\u05e4\u05d5\u05e8\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json new file mode 100644 index 00000000000..689babb357c --- /dev/null +++ b/homeassistant/components/onvif/translations/it.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo ONVIF \u00e8 gi\u00e0 configurato.", + "already_in_progress": "Il flusso di configurazione per il dispositivo ONVIF \u00e8 gi\u00e0 in corso.", + "no_h264": "Non c'erano flussi H264 disponibili. Controllare la configurazione del profilo sul dispositivo.", + "no_mac": "Impossibile configurare l'ID univoco per il dispositivo ONVIF.", + "onvif_error": "Errore durante la configurazione del dispositivo ONVIF. Controllare i registri per ulteriori informazioni." + }, + "error": { + "connection_failed": "Impossibile connettersi al servizio ONVIF con le credenziali fornite." + }, + "step": { + "auth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Configurare l'autenticazione" + }, + "configure_profile": { + "data": { + "include": "Crea entit\u00e0 telecamera" + }, + "description": "Creare un'entit\u00e0 telecamera per {profile} alla risoluzione {resolution}?", + "title": "Configurare i Profili" + }, + "device": { + "data": { + "host": "Seleziona il dispositivo ONVIF rilevato" + }, + "title": "Selezionare il dispositivo ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Porta" + }, + "title": "Configurare il dispositivo ONVIF" + }, + "user": { + "description": "Facendo clic su Invia, cercheremo nella tua rete i dispositivi ONVIF che supportano il profilo S. \n\nAlcuni produttori hanno iniziato a disabilitare ONVIF per impostazione predefinita. Assicurati che ONVIF sia abilitato nella configurazione della tua telecamera.", + "title": "Configurazione del dispositivo ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argomenti FFMPEG aggiuntivi", + "rtsp_transport": "Meccanismo di trasporto RTSP" + }, + "title": "Opzioni dispositivo ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ko.json b/homeassistant/components/onvif/translations/ko.json new file mode 100644 index 00000000000..9715d89f72b --- /dev/null +++ b/homeassistant/components/onvif/translations/ko.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_in_progress": "ONVIF \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "no_h264": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c H264 \uc2a4\ud2b8\ub9bc\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\uc5d0\uc11c \ud504\ub85c\ud544 \uad6c\uc131\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "no_mac": "ONVIF \uae30\uae30\uc758 \uace0\uc720 ID \ub97c \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "onvif_error": "ONVIF \uae30\uae30 \uc124\uc815 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \ub85c\uadf8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "error": { + "connection_failed": "\uc785\ub825\ud558\uc2e0 \uc790\uaca9 \uc99d\uba85\uc73c\ub85c ONVIF \uc11c\ube44\uc2a4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "auth": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "\uc778\uc99d \uad6c\uc131\ud558\uae30" + }, + "configure_profile": { + "data": { + "include": "\uce74\uba54\ub77c \uad6c\uc131\uc694\uc18c \ub9cc\ub4e4\uae30" + }, + "description": "{resolution} \ud574\uc0c1\ub3c4\ub85c {profile} \uce74\uba54\ub77c \uad6c\uc131\uc694\uc18c\ub97c \ub9cc\ub4dc\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ud504\ub85c\ud544 \uad6c\uc131\ud558\uae30" + }, + "device": { + "data": { + "host": "\ubc1c\uacac\ub41c ONVIF \uae30\uae30 \uc120\ud0dd" + }, + "title": "ONVIF \uae30\uae30 \uc120\ud0dd\ud558\uae30" + }, + "manual_input": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "port": "\ud3ec\ud2b8" + }, + "title": "ONVIF \uae30\uae30 \uad6c\uc131\ud558\uae30" + }, + "user": { + "description": "submit \uc744 \ud074\ub9ad\ud558\uba74 \ud504\ub85c\ud544 S \ub97c \uc9c0\uc6d0\ud558\ub294 ONVIF \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uac80\uc0c9\ud569\ub2c8\ub2e4. \n\n\uc77c\ubd80 \uc81c\uc870\uc5c5\uccb4\ub294 \uae30\ubcf8\uac12\uc73c\ub85c ONVIF \ub97c \ube44\ud65c\uc131\ud654 \ud574 \ub193\uc740 \uacbd\uc6b0\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uce74\uba54\ub77c \uad6c\uc131\uc5d0\uc11c ONVIF \uac00 \ud65c\uc131\ud654\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "title": "ONVIF \uae30\uae30 \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\ucd94\uac00 FFMPEG \uc778\uc218", + "rtsp_transport": "RTSP \uc804\uc1a1 \uba54\ucee4\ub2c8\uc998" + }, + "title": "ONVIF \uae30\uae30 \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/lb.json b/homeassistant/components/onvif/translations/lb.json new file mode 100644 index 00000000000..fb91a80dd1c --- /dev/null +++ b/homeassistant/components/onvif/translations/lb.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF Apparat ass scho konfigur\u00e9iert.", + "already_in_progress": "Konfiguratioun's Oflaf fir den ONVIF Apparat ass schonn am gaangen.", + "no_h264": "Keng H264 Streams disponibel. Iwwerpr\u00e9if Profil Konfiguratioun op dengem Apparat.", + "no_mac": "Konnt keng eenzegarteg ID ariichte fir den ONVIF Apparat.", + "onvif_error": "Feeler beim ariichten vum ONVIF Apparat. Kuck d'Logs fir m\u00e9i Informatiounen." + }, + "error": { + "connection_failed": "Konnt sech net mam ONVIF Service mat den ugebueden Umeldungsinformatiounen verbannen." + }, + "step": { + "auth": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Authentifikatioun konfigur\u00e9ieren" + }, + "configure_profile": { + "data": { + "include": "Kamera Entit\u00e9it erstellen" + }, + "description": "Kamera Entit\u00e9it fir {profile} mat {resolution} Opl\u00e9isung erstellen?", + "title": "Profiler konfigur\u00e9ieren" + }, + "device": { + "data": { + "host": "Entdeckten ONVIF Apparat auswielen" + }, + "title": "ONVIF Apparat auswielen" + }, + "manual_input": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "title": "ONVIF Apparat ariichten" + }, + "user": { + "description": "Andeems du op ofsch\u00e9cke klicks, g\u00ebtt dain Netzwierk fir ONVIF Apparater duerchsicht d\u00e9i den Profile S. \u00ebnnerst\u00ebtzen.\n\nVerschidde Hiersteller hunn ugefaang ONVIF standardm\u00e9isseg ze d\u00e9aktiv\u00e9ieren. Stell s\u00e9cher dass ONVIF an denger Kamera ugeschalt ass.", + "title": "ONVIF Apparat ariichten" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG Argumenter", + "rtsp_transport": "RTSP Transport Mechanismus" + }, + "title": "ONVIF Apparat Optiounen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json new file mode 100644 index 00000000000..90a8c4d4993 --- /dev/null +++ b/homeassistant/components/onvif/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Configureer authenticatie" + }, + "configure_profile": { + "data": { + "include": "Cameraentiteit maken" + }, + "title": "Configureer profielen" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Poort" + } + } + } + }, + "options": { + "step": { + "onvif_devices": { + "title": "[%%] Apparaatopties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json new file mode 100644 index 00000000000..5f55b264117 --- /dev/null +++ b/homeassistant/components/onvif/translations/no.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF-enheten er allerede konfigurert.", + "already_in_progress": "Konfigurasjonsflyt for ONVIF-enhet p\u00e5g\u00e5r allerede.", + "no_h264": "Det var ingen H264-str\u00f8mmer tilgjengelig. Sjekk profilkonfigurasjonen p\u00e5 enheten din.", + "no_mac": "Kunne ikke konfigurere unik ID for ONVIF-enhet.", + "onvif_error": "Feil ved konfigurering av ONVIF-enhet. Sjekk logger for mer informasjon." + }, + "error": { + "connection_failed": "Kan ikke koble til ONVIF-tjenesten med angitt legitimasjon." + }, + "step": { + "auth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Konfigurere godkjenning" + }, + "configure_profile": { + "data": { + "include": "Lag kameraentitet" + }, + "description": "Lage kameraentitet for {profile} med {resolution} oppl\u00f8sning?", + "title": "Konfigurer profiler" + }, + "device": { + "data": { + "host": "Velg oppdaget ONVIF-enhet" + }, + "title": "Velg ONVIF-enhet" + }, + "manual_input": { + "data": { + "host": "Vert", + "name": "Navn", + "port": "Port" + }, + "title": "Konfigurere ONVIF-enhet" + }, + "user": { + "description": "Ved \u00e5 klikke send inn, vil vi s\u00f8ke nettverket etter ONVIF-enheter som st\u00f8tter Profil S.\n\nNoen produsenter har begynt \u00e5 deaktivere ONVIF som standard. Vennligst kontroller at ONVIF er aktivert i kameraets konfigurasjon.", + "title": "ONVIF enhetsoppsett" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Ekstra FFMPEG-argumenter", + "rtsp_transport": "RTSP transportmekanisme" + }, + "title": "ONVIF enhetsalternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json new file mode 100644 index 00000000000..afd4df73b66 --- /dev/null +++ b/homeassistant/components/onvif/translations/pl.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Proces konfiguracji dla urz\u0105dzenia ONVIF jest ju\u017c w toku.", + "no_h264": "Nie by\u0142o dost\u0119pnych \u017cadnych strumieni H264. Sprawd\u017a konfiguracj\u0119 profilu w swoim urz\u0105dzeniu.", + "no_mac": "Nie mo\u017cna utworzy\u0107 unikalnego identyfikatora urz\u0105dzenia ONVIF.", + "onvif_error": "Wyst\u0105pi\u0142 b\u0142\u0105d podczas konfigurowania urz\u0105dzenia ONVIF. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji." + }, + "error": { + "connection_failed": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z us\u0142ug\u0105 ONVIF z podanymi po\u015bwiadczeniami." + }, + "step": { + "auth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfigurowanie uwierzytelniania" + }, + "configure_profile": { + "data": { + "include": "Utw\u00f3rz encj\u0119 kamery" + }, + "description": "Czy utworzy\u0107 encj\u0119 kamery dla {profile} o rozdzielczo\u015bci {resolution}?", + "title": "Konfigurowanie profili" + }, + "device": { + "data": { + "host": "Wybierz odnalezione urz\u0105dzenie ONVIF" + }, + "title": "Wybierz urz\u0105dzenie ONVIF" + }, + "manual_input": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "title": "Konfigurowanie urz\u0105dzenia ONVIF" + }, + "user": { + "description": "Klikaj\u0105c przycisk Prze\u015blij, Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", + "title": "Konfiguracja urz\u0105dzenia ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Dodatkowe argumenty FFMPEG", + "rtsp_transport": "Mechanizm transportu RTSP" + }, + "title": "Opcje urz\u0105dzenia ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pt-BR.json b/homeassistant/components/onvif/translations/pt-BR.json new file mode 100644 index 00000000000..3eb03c86f52 --- /dev/null +++ b/homeassistant/components/onvif/translations/pt-BR.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo ONVIF j\u00e1 est\u00e1 configurado.", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o para dispositivos ONVIF j\u00e1 est\u00e1 em andamento.", + "no_h264": "N\u00e3o h\u00e1 fluxos H264 dispon\u00edveis. Verifique a configura\u00e7\u00e3o do perfil no seu dispositivo.", + "no_mac": "N\u00e3o foi poss\u00edvel configurar um ID \u00fanico para o dispositivo ONVIF.", + "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." + }, + "error": { + "connection_failed": "N\u00e3o foi poss\u00edvel conectar ao servi\u00e7o ONVIF com as credenciais fornecidas." + }, + "step": { + "auth": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + }, + "title": "Configurar autentica\u00e7\u00e3o" + }, + "configure_profile": { + "data": { + "include": "Criar entidade c\u00e2mera" + }, + "description": "Criar entidade c\u00e2mera para {profile} com {resolution} de resolu\u00e7\u00e3o?", + "title": "Configurar Perfis" + }, + "device": { + "data": { + "host": "Selecionar dispositivo ONVIF encontrado" + }, + "title": "Selecionar dispositivo ONVIF" + }, + "manual_input": { + "data": { + "host": "Endere\u00e7o (IP)", + "port": "Porta" + }, + "title": "Configurar dispositivo ONVIF" + }, + "user": { + "description": "Ao clicar em enviar, procuraremos na sua rede por dispositivos ONVIF compat\u00edveis com o Perfil S. \n\nAlguns fabricantes deixam o ONVIF desativado por padr\u00e3o. Verifique se o ONVIF est\u00e1 ativado na configura\u00e7\u00e3o da sua c\u00e2mera.", + "title": "Configura\u00e7\u00e3o do dispositivo ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argumentos FFMPEG extras", + "rtsp_transport": "Mecanismo de transporte RTSP" + }, + "title": "Op\u00e7\u00f5es do dispositivo ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json new file mode 100644 index 00000000000..84ff1775080 --- /dev/null +++ b/homeassistant/components/onvif/translations/ru.json @@ -0,0 +1,59 @@ +{ + "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.", + "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.", + "no_h264": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043f\u043e\u0442\u043e\u043a\u043e\u0432 H264. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435.", + "no_mac": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "onvif_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "error": { + "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u043b\u0443\u0436\u0431\u0435 ONVIF \u0441 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u043c\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438." + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + }, + "configure_profile": { + "data": { + "include": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u044b" + }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u044b \u0434\u043b\u044f {profile} \u0441 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c {resolution}?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439" + }, + "device": { + "data": { + "host": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ONVIF" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ONVIF" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + }, + "user": { + "description": "\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u043d\u0430\u0436\u043c\u0451\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c, \u043d\u0430\u0447\u043d\u0451\u0442\u0441\u044f \u043f\u043e\u0438\u0441\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 ONVIF, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 Profile S.\n\n\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u044e\u0442 ONVIF. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e ONVIF \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0412\u0430\u0448\u0435\u0439 \u043a\u0430\u043c\u0435\u0440\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b FFMPEG", + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c RTSP" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/sl.json b/homeassistant/components/onvif/translations/sl.json new file mode 100644 index 00000000000..f7e2df98556 --- /dev/null +++ b/homeassistant/components/onvif/translations/sl.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava ONVIF je \u017ee konfigurirana.", + "already_in_progress": "Konfiguracijski tok za napravo ONVIF je \u017ee v teku.", + "no_h264": "Ni bilo nobenih H264 tokov na voljo. Preverite konfiguracijo profila v napravi.", + "no_mac": "Edinstvenega ID-ja za napravo ONVIF ni bilo mogo\u010de konfigurirati.", + "onvif_error": "Napaka pri nastavitvi naprave ONVIF. Za ve\u010d informacij preverite dnevnike." + }, + "error": { + "connection_failed": "Ni bilo mogo\u010de povezati s storitvijo ONVIF s predlo\u017eenimi poverilnicami." + }, + "step": { + "auth": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "title": "Nastavite preverjanje pristnosti" + }, + "configure_profile": { + "data": { + "include": "Ustvari entiteto kamere" + }, + "description": "Ali ustvarite entiteto kamere za {profile} pri {resolution} lo\u010dljivosti?", + "title": "Nastavite Profile" + }, + "device": { + "data": { + "host": "Izberite odkrito napravo ONVIF" + }, + "title": "Izberite ONVIF napravo" + }, + "manual_input": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + }, + "title": "Konfigurirajte ONVIF napravo" + }, + "user": { + "description": "S klikom na oddajo bomo v omre\u017eju iskali naprave ONVIF, ki podpirajo profil S. \n\n Nekateri proizvajalci so ONVIF privzeto za\u010deli onemogo\u010dati. Prepri\u010dajte se, da je ONVIF omogo\u010den v konfiguraciji naprave.", + "title": "Nastavitev naprave ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Dodatni argumenti FFMPEG", + "rtsp_transport": "RTSP transportni mehanizem" + }, + "title": "Mo\u017enosti naprave ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json new file mode 100644 index 00000000000..0eff403bf45 --- /dev/null +++ b/homeassistant/components/onvif/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "configure_profile": { + "title": "Konfigurera Profiler" + }, + "manual_input": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/zh-Hans.json b/homeassistant/components/onvif/translations/zh-Hans.json new file mode 100644 index 00000000000..c5269fccbd2 --- /dev/null +++ b/homeassistant/components/onvif/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "onvif_devices": { + "title": "ONVIF \u8bbe\u5907\u9009\u9879" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json new file mode 100644 index 00000000000..c7a0f88d1b9 --- /dev/null +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "already_in_progress": "ONVIF \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "no_h264": "\u8a72\u8a2d\u5099\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a2d\u5b9a\u3002", + "no_mac": "\u7121\u6cd5\u70ba ONVIF \u8a2d\u5099\u8a2d\u5b9a\u552f\u4e00 ID\u3002", + "onvif_error": "\u8a2d\u5b9a ONVIF \u8a2d\u5099\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + }, + "error": { + "connection_failed": "\u7121\u6cd5\u4ee5\u6240\u63d0\u4f9b\u7684\u6191\u8b49\u9023\u7dda\u81f3 ONVIF \u670d\u52d9\u3002" + }, + "step": { + "auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a\u9a57\u8b49" + }, + "configure_profile": { + "data": { + "include": "\u65b0\u589e\u651d\u5f71\u6a5f\u7269\u4ef6" + }, + "description": "\u4ee5 {profile} \u4f7f\u7528\u89e3\u6790\u5ea6 {resolution} \u65b0\u589e\u651d\u5f71\u6a5f\u7269\u4ef6\uff1f", + "title": "\u8a2d\u5b9a Profiles" + }, + "device": { + "data": { + "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u8a2d\u5099" + }, + "title": "\u9078\u64c7 ONVIF \u8a2d\u5099" + }, + "manual_input": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a ONVIF \u8a2d\u5099" + }, + "user": { + "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u8a2d\u5099\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002", + "title": "ONVIF \u8a2d\u5099\u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u984d\u5916 FFMPEG \u53c3\u6578", + "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a" + }, + "title": "ONVIF \u8a2d\u5099\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index ed54c7d05ee..37398c61686 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,6 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.18.2", "opencv-python-headless==4.2.0.32"], + "requirements": ["numpy==1.18.4", "opencv-python-headless==4.2.0.32"], "codeowners": [] } diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index e34f98c87d2..239697a229c 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -1,7 +1,7 @@ """Platform for the opengarage.io cover component.""" import logging -import requests +import opengarage import voluptuous as vol from homeassistant.components.cover import ( @@ -9,7 +9,7 @@ from homeassistant.components.cover import ( PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, - CoverDevice, + CoverEntity, ) from homeassistant.const import ( CONF_COVERS, @@ -23,6 +23,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -60,36 +61,34 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = config.get(CONF_COVERS) for device_config in devices.values(): - args = { - CONF_NAME: device_config.get(CONF_NAME), - CONF_HOST: device_config.get(CONF_HOST), - CONF_PORT: device_config.get(CONF_PORT), - CONF_SSL: device_config[CONF_SSL], - CONF_VERIFY_SSL: device_config.get(CONF_VERIFY_SSL), - CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY), - } + opengarage_url = ( + f"{'https' if device_config[CONF_SSL] else 'http'}://" + f"{device_config.get(CONF_HOST)}:{device_config.get(CONF_PORT)}" + ) - covers.append(OpenGarageCover(args)) + open_garage = opengarage.OpenGarage( + opengarage_url, + device_config[CONF_DEVICE_KEY], + device_config[CONF_VERIFY_SSL], + async_get_clientsession(hass), + ) + + covers.append(OpenGarageCover(device_config.get(CONF_NAME), open_garage)) add_entities(covers, True) -class OpenGarageCover(CoverDevice): +class OpenGarageCover(CoverEntity): """Representation of a OpenGarage cover.""" - def __init__(self, args): + def __init__(self, name, open_garage): """Initialize the cover.""" - self.opengarage_url = ( - f"{'https' if args[CONF_SSL] else 'http'}://" - f"{args[CONF_HOST]}:{args[CONF_PORT]}" - ) - self._name = args[CONF_NAME] - self._device_key = args[CONF_DEVICE_KEY] + self._name = name + self._open_garage = open_garage self._state = None self._state_before_move = None self._device_state_attributes = {} self._available = True - self._verify_ssl = args[CONF_VERIFY_SSL] @property def name(self): @@ -113,30 +112,27 @@ class OpenGarageCover(CoverDevice): return None return self._state in [STATE_CLOSED, STATE_OPENING] - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the cover.""" if self._state in [STATE_CLOSED, STATE_CLOSING]: return self._state_before_move = self._state self._state = STATE_CLOSING - self._push_button() + await self._push_button() - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" if self._state in [STATE_OPEN, STATE_OPENING]: return self._state_before_move = self._state self._state = STATE_OPENING - self._push_button() + await self._push_button() - def update(self): + async def async_update(self): """Get updated status from API.""" - try: - status = requests.get(f"{self.opengarage_url}/jc", timeout=10).json() - except requests.exceptions.RequestException as ex: - _LOGGER.error( - "Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex) - ) + status = await self._open_garage.update_state() + if status is None: + _LOGGER.error("Unable to connect to OpenGarage device") self._available = False return @@ -160,19 +156,11 @@ class OpenGarageCover(CoverDevice): self._available = True - def _push_button(self): + async def _push_button(self): """Send commands to API.""" - result = -1 - try: - result = requests.get( - f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", - timeout=10, - verify=self._verify_ssl, - ).json()["result"] - except requests.exceptions.RequestException as ex: - _LOGGER.error( - "Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex) - ) + result = await self._open_garage.push_button() + if result is None: + _LOGGER.error("Unable to connect to OpenGarage device") if result == 1: return diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 1cd5847dd9d..8bbf8c76c42 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -2,5 +2,8 @@ "domain": "opengarage", "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", - "codeowners": [] + "codeowners": [ + "@danielhiversen" + ], + "requirements": ["open-garage==0.1.4"] } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 8456ae9338d..4225228e271 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -3,7 +3,7 @@ import logging from openhomedevice.Device import Device -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class OpenhomeDevice(MediaPlayerDevice): +class OpenhomeDevice(MediaPlayerEntity): """Representation of an Openhome device.""" def __init__(self, hass, device): diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 62c0d3dd2c1..9e3c4d41229 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -1,7 +1,7 @@ """Support for OpenTherm Gateway binary sensors.""" import logging -from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class OpenThermBinarySensor(BinarySensorDevice): +class OpenThermBinarySensor(BinarySensorEntity): """Represent an OpenTherm Gateway binary sensor.""" def __init__(self, gw_dev, var, device_class, friendly_name_format): diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index a7e7eedef34..64625541352 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -3,7 +3,7 @@ import logging from pyotgw import vars as gw_vars -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -51,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(ents) -class OpenThermClimate(ClimateDevice): +class OpenThermClimate(ClimateEntity): """Representation of a climate device.""" def __init__(self, gw_dev, options): diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 41a155b147f..6660c93767e 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -24,7 +24,7 @@ "floor_temperature": "Temperatura de la planta", "precision": "Precisi\u00f3" }, - "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d\u2019OpenTherm" + "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d'OpenTherm" } } } diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json new file mode 100644 index 00000000000..9338998d377 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "La identificaci\u00f3n de la puerta ya existe", + "serial_error": "Error al conectarse al dispositivo", + "timeout": "Tiempo de intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "id": "Identificaci\u00f3n", + "name": "Nombre" + }, + "title": "OpenTherm Gateway" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del piso", + "precision": "Precisi\u00f3n" + }, + "description": "Opciones para OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 07cc2f56ed6..f0ecf0277b2 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -13,7 +13,7 @@ "id": "", "name": "Navn" }, - "title": "OpenTherm Gateway" + "title": "" } } }, diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 6e403a59b43..2d514b33cf3 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -1,7 +1,7 @@ """Support for OpenUV binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.util.dt import as_local, parse_datetime, utcnow @@ -37,7 +37,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(binary_sensors, True) -class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): +class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): """Define a binary sensor for OpenUV.""" def __init__(self, openuv, sensor_type, name, icon, entry_id): diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 2d4b530cd55..0c4913cf6c1 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -4,7 +4,7 @@ "user": { "title": "Fill in your information", "data": { - "api_key": "OpenUV API Key", + "api_key": "[%key:common::config_flow::data::api_key%]", "elevation": "Elevation", "latitude": "Latitude", "longitude": "Longitude" @@ -16,4 +16,4 @@ "invalid_api_key": "Invalid API key" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/en.json b/homeassistant/components/openuv/translations/en.json index c493a15b05a..4c59a587fcd 100644 --- a/homeassistant/components/openuv/translations/en.json +++ b/homeassistant/components/openuv/translations/en.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "OpenUV API Key", + "api_key": "API Key", "elevation": "Elevation", "latitude": "Latitude", "longitude": "Longitude" diff --git a/homeassistant/components/openuv/translations/fi.json b/homeassistant/components/openuv/translations/fi.json new file mode 100644 index 00000000000..65b313a059b --- /dev/null +++ b/homeassistant/components/openuv/translations/fi.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Koordinaatit on jo rekister\u00f6ity", + "invalid_api_key": "Virheellinen API-avain" + }, + "step": { + "user": { + "data": { + "api_key": "OpenUV API-avain", + "elevation": "Korkeus merenpinnasta", + "latitude": "Leveysaste", + "longitude": "Pituusaste" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/ko.json b/homeassistant/components/openuv/translations/ko.json index 77f87a19e0c..46c0e2f0526 100644 --- a/homeassistant/components/openuv/translations/ko.json +++ b/homeassistant/components/openuv/translations/ko.json @@ -7,12 +7,12 @@ "step": { "user": { "data": { - "api_key": "OpenUV API \ud0a4", + "api_key": "API \ud0a4", "elevation": "\uace0\ub3c4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30" } } } diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json index 7e8691ebf26..871f701b399 100644 --- a/homeassistant/components/openuv/translations/pl.json +++ b/homeassistant/components/openuv/translations/pl.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "Klucz API OpenUV", + "api_key": "[%key_id:common::config_flow::data::api_key%] OpenUV", "elevation": "Wysoko\u015b\u0107", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" diff --git a/homeassistant/components/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index faba0c7f6e5..d9d4abc0161 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "OpenUV API \u5bc6\u9470", + "api_key": "API \u5bc6\u9470", "elevation": "\u6d77\u62d4", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6" diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 9ee53704d10..bd0f40da20b 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Init light %s %s", host, entity.unique_id) -class OppleLight(Light): +class OppleLight(LightEntity): """Opple light device.""" def __init__(self, name, host): diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py index b89442a571c..0c8f8bc69cf 100644 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -1,6 +1,6 @@ """Support for binary sensor using Orange Pi GPIO.""" -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from . import edge_detect, read_input, setup_input, setup_mode from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA @@ -24,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(binary_sensors) -class OPiGPIOBinarySensor(BinarySensorDevice): +class OPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses Orange Pi GPIO.""" def __init__(self, hass, name, port, invert_logic): diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index 75a95e053ae..fec30cdade7 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -4,7 +4,7 @@ import logging from orvibo.s20 import S20, S20Exception, discover 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_DISCOVERY, CONF_HOST, @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): add_entities_callback(switches) -class S20Switch(SwitchDevice): +class S20Switch(SwitchEntity): """Representation of an S20 switch.""" def __init__(self, name, s20): diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index ed79604a3f8..49c32da69bc 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv @@ -168,7 +168,7 @@ def setup_bridge(bridge, add_entities, config): update_groups() -class Luminary(Light): +class Luminary(LightEntity): """Representation of Luminary Lights and Groups.""" def __init__(self, luminary, update_func, changed): diff --git a/homeassistant/components/owntracks/translations/ko.json b/homeassistant/components/owntracks/translations/ko.json index 72001c7de85..107e73b98a9 100644 --- a/homeassistant/components/owntracks/translations/ko.json +++ b/homeassistant/components/owntracks/translations/ko.json @@ -9,7 +9,7 @@ "step": { "user": { "description": "OwnTracks \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "OwnTracks \uc124\uc815" + "title": "OwnTracks \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/owntracks/translations/no.json b/homeassistant/components/owntracks/translations/no.json index d03ee6889cd..738c8d3c034 100644 --- a/homeassistant/components/owntracks/translations/no.json +++ b/homeassistant/components/owntracks/translations/no.json @@ -4,7 +4,7 @@ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { - "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url} \n - Identifikasjon: \n - Brukernavn: ` ` \n - Enhets-ID: ` ` \n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP \n - URL: {webhook_url} \n - Sl\u00e5 p\u00e5 autentisering \n - BrukerID: ` ` \n\n {secret} \n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." + "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ''\n - Device ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." }, "step": { "user": { diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py new file mode 100644 index 00000000000..3a7d35ddd1d --- /dev/null +++ b/homeassistant/components/ozw/__init__.py @@ -0,0 +1,330 @@ +"""The ozw integration.""" +import asyncio +import json +import logging + +from openzwavemqtt import OZWManager, OZWOptions +from openzwavemqtt.const import ( + EVENT_INSTANCE_EVENT, + EVENT_NODE_ADDED, + EVENT_NODE_CHANGED, + EVENT_NODE_REMOVED, + EVENT_VALUE_ADDED, + EVENT_VALUE_CHANGED, + EVENT_VALUE_REMOVED, + CommandClass, + ValueType, +) +from openzwavemqtt.models.node import OZWNode +from openzwavemqtt.models.value import OZWValue +import voluptuous as vol + +from homeassistant.components import mqtt +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from . import const +from .const import DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS, TOPIC_OPENZWAVE +from .discovery import DISCOVERY_SCHEMAS, check_node_schema, check_value_schema +from .entity import ( + ZWaveDeviceEntityValues, + create_device_id, + create_device_name, + create_value_id, +) +from .services import ZWaveServices + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +DATA_DEVICES = "zwave-mqtt-devices" + + +async def async_setup(hass: HomeAssistant, config: dict): + """Initialize basic config of ozw component.""" + if "mqtt" not in hass.config.components: + _LOGGER.error("MQTT integration is not set up") + return False + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up ozw from a config entry.""" + ozw_data = hass.data[DOMAIN][entry.entry_id] = {} + ozw_data[DATA_UNSUBSCRIBE] = [] + + data_nodes = {} + data_values = {} + removed_nodes = [] + + @callback + def send_message(topic, payload): + mqtt.async_publish(hass, topic, json.dumps(payload)) + + options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") + manager = OZWManager(options) + + @callback + def async_node_added(node): + # Caution: This is also called on (re)start. + _LOGGER.debug("[NODE ADDED] node_id: %s", node.id) + data_nodes[node.id] = node + if node.id not in data_values: + data_values[node.id] = [] + + @callback + def async_node_changed(node): + _LOGGER.debug("[NODE CHANGED] node_id: %s", node.id) + data_nodes[node.id] = node + # notify devices about the node change + if node.id not in removed_nodes: + hass.async_create_task(async_handle_node_update(hass, node)) + + @callback + def async_node_removed(node): + _LOGGER.debug("[NODE REMOVED] node_id: %s", node.id) + data_nodes.pop(node.id) + # node added/removed events also happen on (re)starts of hass/mqtt/ozw + # cleanup device/entity registry if we know this node is permanently deleted + # entities itself are removed by the values logic + if node.id in removed_nodes: + hass.async_create_task(async_handle_remove_node(hass, node)) + removed_nodes.remove(node.id) + + @callback + def async_instance_event(message): + event = message["event"] + event_data = message["data"] + _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data) + # The actual removal action of a Z-Wave node is reported as instance event + # Only when this event is detected we cleanup the device and entities from hass + if event == "removenode" and "Node" in event_data: + removed_nodes.append(event_data["Node"]) + + @callback + def async_value_added(value): + node = value.node + # Clean up node.node_id and node.id use. They are the same. + node_id = value.node.node_id + + # Filter out CommandClasses we're definitely not interested in. + if value.command_class in [ + CommandClass.CONFIGURATION, + CommandClass.VERSION, + CommandClass.MANUFACTURER_SPECIFIC, + ]: + return + + _LOGGER.debug( + "[VALUE ADDED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", + value.node.id, + value.label, + value.value, + value.value_id_key, + value.command_class, + ) + + node_data_values = data_values[node_id] + + # Check if this value should be tracked by an existing entity + value_unique_id = create_value_id(value) + for values in node_data_values: + values.async_check_value(value) + if values.values_id == value_unique_id: + return # this value already has an entity + + # Run discovery on it and see if any entities need created + for schema in DISCOVERY_SCHEMAS: + if not check_node_schema(node, schema): + continue + if not check_value_schema( + value, schema[const.DISC_VALUES][const.DISC_PRIMARY] + ): + continue + + values = ZWaveDeviceEntityValues(hass, options, schema, value) + values.async_setup() + + # This is legacy and can be cleaned up since we are in the main thread: + # We create a new list and update the reference here so that + # the list can be safely iterated over in the main thread + data_values[node_id] = node_data_values + [values] + + @callback + def async_value_changed(value): + # if an entity belonging to this value needs updating, + # it's handled within the entity logic + _LOGGER.debug( + "[VALUE CHANGED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", + value.node.id, + value.label, + value.value, + value.value_id_key, + value.command_class, + ) + # Handle a scene activation message + if value.command_class in [ + CommandClass.SCENE_ACTIVATION, + CommandClass.CENTRAL_SCENE, + ]: + async_handle_scene_activated(hass, value) + return + + @callback + def async_value_removed(value): + _LOGGER.debug( + "[VALUE REMOVED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", + value.node.id, + value.label, + value.value, + value.value_id_key, + value.command_class, + ) + # signal all entities using this value for removal + value_unique_id = create_value_id(value) + async_dispatcher_send(hass, const.SIGNAL_DELETE_ENTITY, value_unique_id) + # remove value from our local list + node_data_values = data_values[value.node.id] + node_data_values[:] = [ + item for item in node_data_values if item.values_id != value_unique_id + ] + + # Listen to events for node and value changes + options.listen(EVENT_NODE_ADDED, async_node_added) + options.listen(EVENT_NODE_CHANGED, async_node_changed) + options.listen(EVENT_NODE_REMOVED, async_node_removed) + options.listen(EVENT_VALUE_ADDED, async_value_added) + options.listen(EVENT_VALUE_CHANGED, async_value_changed) + options.listen(EVENT_VALUE_REMOVED, async_value_removed) + options.listen(EVENT_INSTANCE_EVENT, async_instance_event) + + # Register Services + services = ZWaveServices(hass, manager) + services.async_register() + + @callback + def async_receive_message(msg): + manager.receive_message(msg.topic, msg.payload) + + async def start_platforms(): + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] + ) + ozw_data[DATA_UNSUBSCRIBE].append( + await mqtt.async_subscribe( + hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message + ) + ) + + hass.async_create_task(start_platforms()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + # cleanup platforms + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + # unsubscribe all listeners + for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: + unsubscribe_listener() + hass.data[DOMAIN].pop(entry.entry_id) + + return True + + +async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): + """Handle the removal of a Z-Wave node, removing all traces in device/entity registry.""" + dev_registry = await get_dev_reg(hass) + # grab device in device registry attached to this node + dev_id = create_device_id(node) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + if not device: + return + devices_to_remove = [device.id] + # also grab slave devices (node instances) + for item in dev_registry.devices.values(): + if item.via_device_id == device.id: + devices_to_remove.append(item.id) + # remove all devices in registry related to this node + # note: removal of entity registry is handled by core + for dev_id in devices_to_remove: + dev_registry.async_remove_device(dev_id) + + +async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): + """ + Handle a node updated event from OZW. + + Meaning some of the basic info like name/model is updated. + We want these changes to be pushed to the device registry. + """ + dev_registry = await get_dev_reg(hass) + # grab device in device registry attached to this node + dev_id = create_device_id(node) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + if not device: + return + # update device in device registry with (updated) info + for item in dev_registry.devices.values(): + if item.id != device.id and item.via_device_id != device.id: + continue + dev_name = create_device_name(node) + dev_registry.async_update_device( + item.id, + manufacturer=node.node_manufacturer_name, + model=node.node_product_name, + name=dev_name, + ) + + +@callback +def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): + """Handle a (central) scene activation message.""" + node_id = scene_value.node.id + scene_id = scene_value.index + scene_label = scene_value.label + if scene_value.command_class == CommandClass.SCENE_ACTIVATION: + # legacy/network scene + scene_value_id = scene_value.value + scene_value_label = scene_value.label + else: + # central scene command + if scene_value.type != ValueType.LIST: + return + scene_value_label = scene_value.value["Selected"] + scene_value_id = scene_value.value["Selected_id"] + + _LOGGER.debug( + "[SCENE_ACTIVATED] node_id: %s - scene_id: %s - scene_value_id: %s", + node_id, + scene_id, + scene_value_id, + ) + # Simply forward it to the hass event bus + hass.bus.async_fire( + const.EVENT_SCENE_ACTIVATED, + { + const.ATTR_NODE_ID: node_id, + const.ATTR_SCENE_ID: scene_id, + const.ATTR_SCENE_LABEL: scene_label, + const.ATTR_SCENE_VALUE_ID: scene_value_id, + const.ATTR_SCENE_VALUE_LABEL: scene_value_label, + }, + ) diff --git a/homeassistant/components/ozw/binary_sensor.py b/homeassistant/components/ozw/binary_sensor.py new file mode 100644 index 00000000000..64fd66394f8 --- /dev/null +++ b/homeassistant/components/ozw/binary_sensor.py @@ -0,0 +1,405 @@ +"""Representation of Z-Wave binary_sensors.""" + +import logging + +from openzwavemqtt.const import CommandClass, ValueIndex, ValueType + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + +NOTIFICATION_TYPE = "index" +NOTIFICATION_VALUES = "values" +NOTIFICATION_DEVICE_CLASS = "device_class" +NOTIFICATION_SENSOR_ENABLED = "enabled" +NOTIFICATION_OFF_VALUE = "off_value" + +NOTIFICATION_VALUE_CLEAR = 0 + +# Translation from values in Notification CC to binary sensors +# https://github.com/OpenZWave/open-zwave/blob/master/config/NotificationCCTypes.xml +NOTIFICATION_SENSORS = [ + { + # Index 1: Smoke Alarm - Value Id's 1 and 2 + # Assuming here that Value 1 and 2 are not present at the same time + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, + NOTIFICATION_VALUES: [1, 2], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SMOKE, + }, + { + # Index 1: Smoke Alarm - All other Value Id's + # Create as disabled sensors + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, + NOTIFICATION_VALUES: [3, 4, 5, 6, 7, 8], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SMOKE, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 2: Carbon Monoxide - Value Id's 1 and 2 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, + NOTIFICATION_VALUES: [1, 2], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + }, + { + # Index 2: Carbon Monoxide - All other Value Id's + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, + NOTIFICATION_VALUES: [4, 5, 7], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 3: Carbon Dioxide - Value Id's 1 and 2 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, + NOTIFICATION_VALUES: [1, 2], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + }, + { + # Index 3: Carbon Dioxide - All other Value Id's + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, + NOTIFICATION_VALUES: [4, 5, 7], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 4: Heat - Value Id's 1, 2, 5, 6 (heat/underheat) + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, + NOTIFICATION_VALUES: [1, 2, 5, 6], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_HEAT, + }, + { + # Index 4: Heat - All other Value Id's + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, + NOTIFICATION_VALUES: [3, 4, 8, 10, 11], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_HEAT, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 5: Water - Value Id's 1, 2, 3, 4 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, + NOTIFICATION_VALUES: [1, 2, 3, 4], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + }, + { + # Index 5: Water - All other Value Id's + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, + NOTIFICATION_VALUES: [5], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 6: Access Control - Value Id's 1, 2, 3, 4 (Lock) + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, + NOTIFICATION_VALUES: [1, 2, 3, 4], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_LOCK, + }, + { + # Index 6: Access Control - Value Id 22 (door/window open) + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, + NOTIFICATION_VALUES: [22], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_DOOR, + NOTIFICATION_OFF_VALUE: 23, + }, + { + # Index 7: Home Security - Value Id's 1, 2 (intrusion) + # Assuming that value 1 and 2 are not present at the same time + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, + NOTIFICATION_VALUES: [1, 2], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SAFETY, + }, + { + # Index 7: Home Security - Value Id's 3, 4, 9 (tampering) + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, + NOTIFICATION_VALUES: [3, 4, 9], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SAFETY, + }, + { + # Index 7: Home Security - Value Id's 5, 6 (glass breakage) + # Assuming that value 5 and 6 are not present at the same time + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, + NOTIFICATION_VALUES: [5, 6], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SAFETY, + }, + { + # Index 7: Home Security - Value Id's 7, 8 (motion) + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, + NOTIFICATION_VALUES: [7, 8], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_MOTION, + }, + { + # Index 8: Power management - Values 1...9 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, + NOTIFICATION_VALUES: [1, 2, 3, 4, 5, 6, 7, 8, 9], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_POWER, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 8: Power management - Values 10...15 + # Battery values (mutually exclusive) + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, + NOTIFICATION_VALUES: [10, 11, 12, 13, 14, 15], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_POWER, + NOTIFICATION_SENSOR_ENABLED: False, + NOTIFICATION_OFF_VALUE: None, + }, + { + # Index 9: System - Value Id's 1, 2, 6, 7 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SYSTEM, + NOTIFICATION_VALUES: [1, 2, 6, 7], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 10: Emergency - Value Id's 1, 2, 3 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_EMERGENCY, + NOTIFICATION_VALUES: [1, 2, 3], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + }, + { + # Index 11: Clock - Value Id's 1, 2 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CLOCK, + NOTIFICATION_VALUES: [1, 2], + NOTIFICATION_DEVICE_CLASS: None, + NOTIFICATION_SENSOR_ENABLED: False, + }, + { + # Index 12: Appliance - All Value Id's + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE, + NOTIFICATION_VALUES: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + ], + NOTIFICATION_DEVICE_CLASS: None, + }, + { + # Index 13: Home Health - Value Id's 1,2,3,4,5 + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE, + NOTIFICATION_VALUES: [1, 2, 3, 4, 5], + NOTIFICATION_DEVICE_CLASS: None, + }, + { + # Index 14: Siren + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SIREN, + NOTIFICATION_VALUES: [1], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_SOUND, + }, + { + # Index 15: Water valve + # ignore non-boolean values + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER_VALVE, + NOTIFICATION_VALUES: [3, 4], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + }, + { + # Index 16: Weather + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WEATHER, + NOTIFICATION_VALUES: [1, 2], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + }, + { + # Index 17: Irrigation + # ignore non-boolean values + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_IRRIGATION, + NOTIFICATION_VALUES: [1, 2, 3, 4, 5], + NOTIFICATION_DEVICE_CLASS: None, + }, + { + # Index 18: Gas + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, + NOTIFICATION_VALUES: [1, 2, 3, 4], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_GAS, + }, + { + # Index 18: Gas + NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, + NOTIFICATION_VALUES: [6], + NOTIFICATION_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, + }, +] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave binary_sensor from config entry.""" + + @callback + def async_add_binary_sensor(values): + """Add Z-Wave Binary Sensor(s).""" + async_add_entities(VALUE_TYPE_SENSORS[values.primary.type](values)) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{BINARY_SENSOR_DOMAIN}", async_add_binary_sensor + ) + ) + + +@callback +def async_get_legacy_binary_sensors(values): + """Add Legacy/classic Z-Wave Binary Sensor.""" + return [ZWaveBinarySensor(values)] + + +@callback +def async_get_notification_sensors(values): + """Convert Notification values into binary sensors.""" + sensors_to_add = [] + for list_value in values.primary.value["List"]: + # check if we have a mapping for this value + for item in NOTIFICATION_SENSORS: + if item[NOTIFICATION_TYPE] != values.primary.index: + continue + if list_value["Value"] not in item[NOTIFICATION_VALUES]: + continue + sensors_to_add.append( + ZWaveListValueSensor( + # required values + values, + list_value["Value"], + item[NOTIFICATION_DEVICE_CLASS], + # optional values + item.get(NOTIFICATION_SENSOR_ENABLED, True), + item.get(NOTIFICATION_OFF_VALUE, NOTIFICATION_VALUE_CLEAR), + ) + ) + return sensors_to_add + + +VALUE_TYPE_SENSORS = { + ValueType.BOOL: async_get_legacy_binary_sensors, + ValueType.LIST: async_get_notification_sensors, +} + + +class ZWaveBinarySensor(ZWaveDeviceEntity, BinarySensorEntity): + """Representation of a Z-Wave binary_sensor.""" + + @property + def is_on(self): + """Return if the sensor is on or off.""" + return self.values.primary.value + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # Legacy binary sensors are phased out (replaced by notification sensors) + # Disable by default to not confuse users + for item in self.values.primary.node.values(): + if item.command_class == CommandClass.NOTIFICATION: + # This device properly implements the Notification CC, legacy sensor can be disabled + return False + return True + + +class ZWaveListValueSensor(ZWaveDeviceEntity, BinarySensorEntity): + """Representation of a binary_sensor from values in the Z-Wave Notification CommandClass.""" + + def __init__( + self, + values, + on_value, + device_class=None, + default_enabled=True, + off_value=NOTIFICATION_VALUE_CLEAR, + ): + """Initialize a ZWaveListValueSensor entity.""" + super().__init__(values) + self._on_value = on_value + self._device_class = device_class + self._default_enabled = default_enabled + self._off_value = off_value + # make sure the correct value is selected at startup + self._state = False + self.on_value_update() + + @callback + def on_value_update(self): + """Call when a value is added/updated in the underlying EntityValues Collection.""" + if self.values.primary.value["Selected_id"] == self._on_value: + # Only when the active ID exactly matches our watched ON value, set sensor state to ON + self._state = True + elif self.values.primary.value["Selected_id"] == self._off_value: + # Only when the active ID exactly matches our watched OFF value, set sensor state to OFF + self._state = False + elif ( + self._off_value is None + and self.values.primary.value["Selected_id"] != self._on_value + ): + # Off value not explicitly specified + # Some values are reset by the simple fact they're overruled by another value coming in + # For example the battery charging values in Power Management Index + self._state = False + + @property + def name(self): + """Return the name of the entity.""" + # Append value label to base name + base_name = super().name + value_label = "" + for item in self.values.primary.value["List"]: + if item["Value"] == self._on_value: + value_label = item["Label"] + break + # Strip "on location" / "at location" from name + # Note: We're assuming that we don't retrieve 2 values with different location + value_label = value_label.split(" on ")[0] + value_label = value_label.split(" at ")[0] + return f"{base_name}: {value_label}" + + @property + def unique_id(self): + """Return the unique_id of the entity.""" + unique_id = super().unique_id + return f"{unique_id}.{self._on_value}" + + @property + def is_on(self): + """Return if the sensor is on or off.""" + return self._state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._device_class + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide the more advanced sensors by default to not overwhelm users + return self._default_enabled diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py new file mode 100644 index 00000000000..8822490132c --- /dev/null +++ b/homeassistant/components/ozw/config_flow.py @@ -0,0 +1,24 @@ +"""Config flow for ozw integration.""" +from homeassistant import config_entries + +from .const import DOMAIN # pylint:disable=unused-import + +TITLE = "OpenZWave" + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for ozw.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="one_instance_allowed") + if "mqtt" not in self.hass.config.components: + return self.async_abort(reason="mqtt_required") + if user_input is not None: + return self.async_create_entry(title=TITLE, data={}) + + return self.async_show_form(step_id="user") diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py new file mode 100644 index 00000000000..59f189d124d --- /dev/null +++ b/homeassistant/components/ozw/const.py @@ -0,0 +1,46 @@ +"""Constants for the ozw integration.""" +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN + +DOMAIN = "ozw" +DATA_UNSUBSCRIBE = "unsubscribe" +PLATFORMS = [BINARY_SENSOR_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] + +# MQTT Topics +TOPIC_OPENZWAVE = "OpenZWave" + +# Common Attributes +ATTR_INSTANCE_ID = "instance_id" +ATTR_SECURE = "secure" +ATTR_NODE_ID = "node_id" +ATTR_SCENE_ID = "scene_id" +ATTR_SCENE_LABEL = "scene_label" +ATTR_SCENE_VALUE_ID = "scene_value_id" +ATTR_SCENE_VALUE_LABEL = "scene_value_label" + +# Service specific +SERVICE_ADD_NODE = "add_node" +SERVICE_REMOVE_NODE = "remove_node" + +# Home Assistant Events +EVENT_SCENE_ACTIVATED = f"{DOMAIN}.scene_activated" + +# Signals +SIGNAL_DELETE_ENTITY = f"{DOMAIN}_delete_entity" + +# Discovery Information +DISC_COMMAND_CLASS = "command_class" +DISC_COMPONENT = "component" +DISC_GENERIC_DEVICE_CLASS = "generic_device_class" +DISC_GENRE = "genre" +DISC_INDEX = "index" +DISC_INSTANCE = "instance" +DISC_NODE_ID = "node_id" +DISC_OPTIONAL = "optional" +DISC_PRIMARY = "primary" +DISC_SCHEMAS = "schemas" +DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" +DISC_TYPE = "type" +DISC_VALUES = "values" diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py new file mode 100644 index 00000000000..38940c9ed6e --- /dev/null +++ b/homeassistant/components/ozw/discovery.py @@ -0,0 +1,151 @@ +"""Map Z-Wave nodes and values to Home Assistant entities.""" +import openzwavemqtt.const as const_ozw +from openzwavemqtt.const import CommandClass, ValueGenre, ValueIndex, ValueType + +from . import const + +DISCOVERY_SCHEMAS = ( + { # Binary sensors + const.DISC_COMPONENT: "binary_sensor", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: CommandClass.SENSOR_BINARY, + const.DISC_TYPE: ValueType.BOOL, + const.DISC_GENRE: ValueGenre.USER, + }, + "off_delay": { + const.DISC_COMMAND_CLASS: CommandClass.CONFIGURATION, + const.DISC_INDEX: 9, + const.DISC_OPTIONAL: True, + }, + }, + }, + { # Notification CommandClass translates to binary_sensor + const.DISC_COMPONENT: "binary_sensor", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: CommandClass.NOTIFICATION, + const.DISC_GENRE: ValueGenre.USER, + const.DISC_TYPE: (ValueType.BOOL, ValueType.LIST), + } + }, + }, + { # Light + const.DISC_COMPONENT: "light", + const.DISC_GENERIC_DEVICE_CLASS: ( + const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, + const_ozw.GENERIC_TYPE_SWITCH_REMOTE, + ), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL, + const_ozw.SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL, + const_ozw.SPECIFIC_TYPE_NOT_USED, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, + const.DISC_TYPE: ValueType.BYTE, + }, + "dimming_duration": { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DURATION, + const.DISC_OPTIONAL: True, + }, + "color": { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,), + const.DISC_INDEX: ValueIndex.SWITCH_COLOR_COLOR, + const.DISC_OPTIONAL: True, + }, + "color_channels": { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,), + const.DISC_INDEX: ValueIndex.SWITCH_COLOR_CHANNELS, + const.DISC_OPTIONAL: True, + }, + }, + }, + { # All other text/numeric sensors + const.DISC_COMPONENT: "sensor", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: ( + CommandClass.SENSOR_MULTILEVEL, + CommandClass.METER, + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + CommandClass.NOTIFICATION, + CommandClass.BASIC, + ), + const.DISC_TYPE: ( + ValueType.DECIMAL, + ValueType.INT, + ValueType.STRING, + ValueType.BYTE, + ValueType.LIST, + ), + } + }, + }, + { # Switch platform + const.DISC_COMPONENT: "switch", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_BINARY,), + const.DISC_TYPE: ValueType.BOOL, + const.DISC_GENRE: ValueGenre.USER, + } + }, + }, +) + + +def check_node_schema(node, schema): + """Check if node matches the passed node schema.""" + if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]: + return False + if const.DISC_GENERIC_DEVICE_CLASS in schema and not eq_or_in( + node.node_generic, schema[const.DISC_GENERIC_DEVICE_CLASS] + ): + return False + if const.DISC_SPECIFIC_DEVICE_CLASS in schema and not eq_or_in( + node.node_specific, schema[const.DISC_SPECIFIC_DEVICE_CLASS] + ): + return False + return True + + +def check_value_schema(value, schema): + """Check if the value matches the passed value schema.""" + if const.DISC_COMMAND_CLASS in schema and not eq_or_in( + value.parent.command_class_id, schema[const.DISC_COMMAND_CLASS] + ): + return False + if const.DISC_TYPE in schema and not eq_or_in(value.type, schema[const.DISC_TYPE]): + return False + if const.DISC_GENRE in schema and not eq_or_in( + value.genre, schema[const.DISC_GENRE] + ): + return False + if const.DISC_INDEX in schema and not eq_or_in( + value.index, schema[const.DISC_INDEX] + ): + return False + if const.DISC_INSTANCE in schema and not eq_or_in( + value.instance, schema[const.DISC_INSTANCE] + ): + return False + if const.DISC_SCHEMAS in schema: + found = False + for schema_item in schema[const.DISC_SCHEMAS]: + found = found or check_value_schema(value, schema_item) + if not found: + return False + + return True + + +def eq_or_in(val, options): + """Return True if options contains value or if value is equal to options.""" + return val in options if isinstance(options, tuple) else val == options diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py new file mode 100644 index 00000000000..d64beb0ba34 --- /dev/null +++ b/homeassistant/components/ozw/entity.py @@ -0,0 +1,295 @@ +"""Generic Z-Wave Entity Classes.""" + +import copy +import logging + +from openzwavemqtt.const import ( + EVENT_INSTANCE_STATUS_CHANGED, + EVENT_VALUE_CHANGED, + OZW_READY_STATES, +) +from openzwavemqtt.models.node import OZWNode +from openzwavemqtt.models.value import OZWValue + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity + +from . import const +from .const import DOMAIN, PLATFORMS +from .discovery import check_node_schema, check_value_schema + +_LOGGER = logging.getLogger(__name__) + + +class ZWaveDeviceEntityValues: + """Manages entity access to the underlying Z-Wave value objects.""" + + def __init__(self, hass, options, schema, primary_value): + """Initialize the values object with the passed entity schema.""" + self._hass = hass + self._entity_created = False + self._schema = copy.deepcopy(schema) + self._values = {} + self.options = options + + # Go through values listed in the discovery schema, initialize them, + # and add a check to the schema to make sure the Instance matches. + for name, disc_settings in self._schema[const.DISC_VALUES].items(): + self._values[name] = None + disc_settings[const.DISC_INSTANCE] = (primary_value.instance,) + + self._values[const.DISC_PRIMARY] = primary_value + self._node = primary_value.node + self._schema[const.DISC_NODE_ID] = [self._node.node_id] + + def async_setup(self): + """Set up values instance.""" + # Check values that have already been discovered for node + # and see if they match the schema and need added to the entity. + for value in self._node.values(): + self.async_check_value(value) + + # Check if all the _required_ values in the schema are present and + # create the entity. + self._async_check_entity_ready() + + def __getattr__(self, name): + """Get the specified value for this entity.""" + return self._values.get(name, None) + + def __iter__(self): + """Allow iteration over all values.""" + return iter(self._values.values()) + + def __contains__(self, name): + """Check if the specified name/key exists in the values.""" + return name in self._values + + @callback + def async_check_value(self, value): + """Check if the new value matches a missing value for this entity. + + If a match is found, it is added to the values mapping. + """ + # Make sure the node matches the schema for this entity. + if not check_node_schema(value.node, self._schema): + return + + # Go through the possible values for this entity defined by the schema. + for name in self._values: + # Skip if it's already been added. + if self._values[name] is not None: + continue + # Skip if the value doesn't match the schema. + if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): + continue + + # Add value to mapping. + self._values[name] = value + + # If the entity has already been created, notify it of the new value. + if self._entity_created: + async_dispatcher_send( + self._hass, f"{DOMAIN}_{self.values_id}_value_added" + ) + + # Check if entity has all required values and create the entity if needed. + self._async_check_entity_ready() + + @callback + def _async_check_entity_ready(self): + """Check if all required values are discovered and create entity.""" + # Abort if the entity has already been created + if self._entity_created: + return + + # Go through values defined in the schema and abort if a required value is missing. + for name, disc_settings in self._schema[const.DISC_VALUES].items(): + if self._values[name] is None and not disc_settings.get( + const.DISC_OPTIONAL + ): + return + + # We have all the required values, so create the entity. + component = self._schema[const.DISC_COMPONENT] + + _LOGGER.debug( + "Adding Node_id=%s Generic_command_class=%s, " + "Specific_command_class=%s, " + "Command_class=%s, Index=%s, Value type=%s, " + "Genre=%s as %s", + self._node.node_id, + self._node.node_generic, + self._node.node_specific, + self.primary.command_class, + self.primary.index, + self.primary.type, + self.primary.genre, + component, + ) + self._entity_created = True + + if component in PLATFORMS: + async_dispatcher_send(self._hass, f"{DOMAIN}_new_{component}", self) + + @property + def values_id(self): + """Identification for this values collection.""" + return create_value_id(self.primary) + + +class ZWaveDeviceEntity(Entity): + """Generic Entity Class for a Z-Wave Device.""" + + def __init__(self, values): + """Initialize a generic Z-Wave device entity.""" + self.values = values + self.options = values.options + + @callback + def on_value_update(self): + """Call when a value is added/updated in the entity EntityValues Collection. + + To be overridden by platforms needing this event. + """ + + async def async_added_to_hass(self): + """Call when entity is added.""" + # add dispatcher and OZW listeners callbacks, + self.options.listen(EVENT_VALUE_CHANGED, self._value_changed) + self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated) + # add to on_remove so they will be cleaned up on entity removal + self.async_on_remove( + async_dispatcher_connect( + self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.values.values_id}_value_added", + self._value_added, + ) + ) + + @property + def device_info(self): + """Return device information for the device registry.""" + node = self.values.primary.node + node_instance = self.values.primary.instance + dev_id = create_device_id(node, self.values.primary.instance) + device_info = { + "identifiers": {(DOMAIN, dev_id)}, + "name": create_device_name(node), + "manufacturer": node.node_manufacturer_name, + "model": node.node_product_name, + } + # device with multiple instances is split up into virtual devices for each instance + if node_instance > 1: + parent_dev_id = create_device_id(node) + device_info["name"] += f" - Instance {node_instance}" + device_info["via_device"] = (DOMAIN, parent_dev_id) + return device_info + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return {const.ATTR_NODE_ID: self.values.primary.node.node_id} + + @property + def name(self): + """Return the name of the entity.""" + node = self.values.primary.node + return f"{create_device_name(node)}: {self.values.primary.label}" + + @property + def unique_id(self): + """Return the unique_id of the entity.""" + return self.values.values_id + + @property + def available(self) -> bool: + """Return entity availability.""" + # Use OZW Daemon status for availability. + instance_status = self.values.primary.ozw_instance.get_status() + return instance_status and instance_status.status in ( + state.value for state in OZW_READY_STATES + ) + + @callback + def _value_changed(self, value): + """Call when a value from ZWaveDeviceEntityValues is changed. + + Should not be overridden by subclasses. + """ + if value.value_id_key in (v.value_id_key for v in self.values if v): + self.on_value_update() + self.async_write_ha_state() + + @callback + def _value_added(self): + """Call when a value from ZWaveDeviceEntityValues is added. + + Should not be overridden by subclasses. + """ + self.on_value_update() + + @callback + def _instance_updated(self, new_status): + """Call when the instance status changes. + + Should not be overridden by subclasses. + """ + self.on_value_update() + self.async_write_ha_state() + + async def _delete_callback(self, values_id): + """Remove this entity.""" + if not self.values: + return # race condition: delete already requested + if values_id == self.values.values_id: + await self.async_remove() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + # cleanup OZW listeners + self.options.listeners[EVENT_VALUE_CHANGED].remove(self._value_changed) + self.options.listeners[EVENT_INSTANCE_STATUS_CHANGED].remove( + self._instance_updated + ) + + +def create_device_name(node: OZWNode): + """Generate sensible (short) default device name from a OZWNode.""" + # Prefer custom name set by OZWAdmin if present + if node.node_name: + return node.node_name + # Prefer short devicename from metadata if present + if node.meta_data and node.meta_data.get("Name"): + return node.meta_data["Name"] + # Fallback to productname or devicetype strings + if node.node_product_name: + return node.node_product_name + if node.node_device_type_string: + return node.node_device_type_string + if node.node_specific_string: + return node.node_specific_string + # Last resort: use Node id (should never happen, but just in case) + return f"Node {node.id}" + + +def create_device_id(node: OZWNode, node_instance: int = 1): + """Generate unique device_id from a OZWNode.""" + ozw_instance = node.parent.id + dev_id = f"{ozw_instance}.{node.node_id}.{node_instance}" + return dev_id + + +def create_value_id(value: OZWValue): + """Generate unique value_id from an OZWValue.""" + # [OZW_INSTANCE_ID]-[NODE_ID]-[VALUE_ID_KEY] + return f"{value.node.parent.id}-{value.node.id}-{value.value_id_key}" diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py new file mode 100644 index 00000000000..640f675612c --- /dev/null +++ b/homeassistant/components/ozw/light.py @@ -0,0 +1,135 @@ +"""Support for Z-Wave lights.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_TRANSITION, + LightEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Light from Config Entry.""" + + @callback + def async_add_light(values): + """Add Z-Wave Light.""" + light = ZwaveDimmer(values) + async_add_entities([light]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_new_{LIGHT_DOMAIN}", async_add_light) + ) + + +def byte_to_zwave_brightness(value): + """Convert brightness in 0-255 scale to 0-99 scale. + + `value` -- (int) Brightness byte value from 0-255. + """ + if value > 0: + return max(1, round((value / 255) * 99)) + return 0 + + +class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): + """Representation of a Z-Wave dimmer.""" + + def __init__(self, values): + """Initialize the light.""" + super().__init__(values) + self._supported_features = SUPPORT_BRIGHTNESS + # make sure that supported features is correctly set + self.on_value_update() + + @callback + def on_value_update(self): + """Call when the underlying value(s) is added or updated.""" + self._supported_features = SUPPORT_BRIGHTNESS + if self.values.dimming_duration is not None: + self._supported_features |= SUPPORT_TRANSITION + + @property + def brightness(self): + """Return the brightness of this light between 0..255. + + Zwave multilevel switches use a range of [0, 99] to control brightness. + """ + if "target" in self.values: + return round((self.values.target.value / 99) * 255) + return round((self.values.primary.value / 99) * 255) + + @property + def is_on(self): + """Return true if device is on (brightness above 0).""" + if "target" in self.values: + return self.values.target.value > 0 + return self.values.primary.value > 0 + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + @callback + def async_set_duration(self, **kwargs): + """Set the transition time for the brightness value. + + Zwave Dimming Duration values: + 0 = instant + 0-127 = 1 second to 127 seconds + 128-254 = 1 minute to 127 minutes + 255 = factory default + """ + if self.values.dimming_duration is None: + return + + if ATTR_TRANSITION not in kwargs: + # no transition specified by user, use defaults + new_value = 255 + else: + # transition specified by user, convert to zwave value + transition = kwargs[ATTR_TRANSITION] + if transition <= 127: + new_value = int(transition) + else: + minutes = int(transition / 60) + _LOGGER.debug( + "Transition rounded to %d minutes for %s", minutes, self.entity_id + ) + new_value = minutes + 128 + + # only send value if it differs from current + # this prevents a command for nothing + if self.values.dimming_duration.value != new_value: + self.values.dimming_duration.send_value(new_value) + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self.async_set_duration(**kwargs) + + # Zwave multilevel switches use a range of [0, 99] to control + # brightness. Level 255 means to set it to previous value. + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + brightness = byte_to_zwave_brightness(brightness) + else: + brightness = 255 + + self.values.primary.send_value(brightness) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + self.async_set_duration(**kwargs) + + self.values.primary.send_value(0) diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json new file mode 100644 index 00000000000..3b828845852 --- /dev/null +++ b/homeassistant/components/ozw/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ozw", + "name": "OpenZWave (beta)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ozw", + "requirements": ["python-openzwave-mqtt==1.0.1"], + "after_dependencies": ["mqtt"], + "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"] +} diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py new file mode 100644 index 00000000000..309c2784405 --- /dev/null +++ b/homeassistant/components/ozw/sensor.py @@ -0,0 +1,131 @@ +"""Representation of Z-Wave sensors.""" + +import logging + +from openzwavemqtt.const import CommandClass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave sensor from config entry.""" + + @callback + def async_add_sensor(value): + """Add Z-Wave Sensor.""" + # Basic Sensor types + if isinstance(value.primary.value, (float, int)): + sensor = ZWaveNumericSensor(value) + + elif isinstance(value.primary.value, dict): + sensor = ZWaveListSensor(value) + + else: + _LOGGER.warning("Sensor not implemented for value %s", value.primary.label) + return + + async_add_entities([sensor]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{SENSOR_DOMAIN}", async_add_sensor + ) + ) + + +class ZwaveSensorBase(ZWaveDeviceEntity): + """Basic Representation of a Z-Wave sensor.""" + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.values.primary.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + if self.values.primary.command_class == CommandClass.METER: + return DEVICE_CLASS_POWER + if "Temperature" in self.values.primary.label: + return DEVICE_CLASS_TEMPERATURE + if "Illuminance" in self.values.primary.label: + return DEVICE_CLASS_ILLUMINANCE + if "Humidity" in self.values.primary.label: + return DEVICE_CLASS_HUMIDITY + if "Power" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Energy" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Electric" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Pressure" in self.values.primary.label: + return DEVICE_CLASS_PRESSURE + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some of the more advanced sensors by default to not overwhelm users + if self.values.primary.command_class in [ + CommandClass.BASIC, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + return False + return True + + +class ZWaveNumericSensor(ZwaveSensorBase): + """Representation of a Z-Wave sensor.""" + + @property + def state(self): + """Return state of the sensor.""" + return round(self.values.primary.value, 2) + + @property + def unit_of_measurement(self): + """Return unit of measurement the value is expressed in.""" + if self.values.primary.units == "C": + return TEMP_CELSIUS + if self.values.primary.units == "F": + return TEMP_FAHRENHEIT + + return self.values.primary.units + + +class ZWaveListSensor(ZwaveSensorBase): + """Representation of a Z-Wave list sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + # We use the id as value for backwards compatibility + return self.values.primary.value["Selected_id"] + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attributes = super().device_state_attributes + # add the value's label as property + attributes["label"] = self.values.primary.value["Selected"] + return attributes + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # these sensors are only here for backwards compatibility, disable them by default + return False diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py new file mode 100644 index 00000000000..a2f4ca2e553 --- /dev/null +++ b/homeassistant/components/ozw/services.py @@ -0,0 +1,53 @@ +"""Methods and classes related to executing Z-Wave commands and publishing these to hass.""" +import voluptuous as vol + +from homeassistant.core import callback + +from . import const + + +class ZWaveServices: + """Class that holds our services ( Zwave Commands) that should be published to hass.""" + + def __init__(self, hass, manager): + """Initialize with both hass and ozwmanager objects.""" + self._hass = hass + self._manager = manager + + @callback + def async_register(self): + """Register all our services.""" + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_ADD_NODE, + self.async_add_node, + schema=vol.Schema( + { + vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), + vol.Optional(const.ATTR_SECURE, default=False): vol.Coerce(bool), + } + ), + ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_REMOVE_NODE, + self.async_remove_node, + schema=vol.Schema( + {vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)} + ), + ) + + @callback + def async_add_node(self, service): + """Enter inclusion mode on the controller.""" + instance_id = service.data[const.ATTR_INSTANCE_ID] + secure = service.data[const.ATTR_SECURE] + instance = self._manager.get_instance(instance_id) + instance.add_node(secure) + + @callback + def async_remove_node(self, service): + """Enter exclusion mode on the controller.""" + instance_id = service.data[const.ATTR_INSTANCE_ID] + instance = self._manager.get_instance(instance_id) + instance.remove_node() diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml new file mode 100644 index 00000000000..92685f1a463 --- /dev/null +++ b/homeassistant/components/ozw/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available Z-Wave services +add_node: + description: Add a new node to the Z-Wave network. + fields: + secure: + description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. + instance_id: + description: (Optional) The OZW Instance/Controller to use, defaults to 1. + +remove_node: + description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode. + fields: + instance_id: + description: (Optional) The OZW Instance/Controller to use, defaults to 1. diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json new file mode 100644 index 00000000000..dd2aad7e4ce --- /dev/null +++ b/homeassistant/components/ozw/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "title": "Confirm set up" + } + }, + "abort": { + "one_instance_allowed": "The integration only supports one Z-Wave instance", + "mqtt_required": "The MQTT integration is not set up" + } + } +} diff --git a/homeassistant/components/ozw/switch.py b/homeassistant/components/ozw/switch.py new file mode 100644 index 00000000000..c1a0ef353b8 --- /dev/null +++ b/homeassistant/components/ozw/switch.py @@ -0,0 +1,41 @@ +"""Representation of Z-Wave switches.""" +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave switch from config entry.""" + + @callback + def async_add_switch(value): + """Add Z-Wave Switch.""" + switch = ZWaveSwitch(value) + + async_add_entities([switch]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{SWITCH_DOMAIN}", async_add_switch + ) + ) + + +class ZWaveSwitch(ZWaveDeviceEntity, SwitchEntity): + """Representation of a Z-Wave switch.""" + + @property + def is_on(self): + """Return a boolean for the state of the switch.""" + return bool(self.values.primary.value) + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + self.values.primary.send_value(True) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + self.values.primary.send_value(False) diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json new file mode 100644 index 00000000000..eba9a7e8757 --- /dev/null +++ b/homeassistant/components/ozw/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", + "one_instance_allowed": "La integraci\u00f3 nom\u00e9s admet una inst\u00e0ncia Z-Wave" + }, + "step": { + "user": { + "title": "Confirmaci\u00f3 de configuraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json new file mode 100644 index 00000000000..79393cbf865 --- /dev/null +++ b/homeassistant/components/ozw/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", + "one_instance_allowed": "Die Integration unterst\u00fctzt nur eine Z-Wave-Instanz" + }, + "step": { + "user": { + "title": "Einrichtung best\u00e4tigen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json new file mode 100644 index 00000000000..c6a45474880 --- /dev/null +++ b/homeassistant/components/ozw/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "The MQTT integration is not set up", + "one_instance_allowed": "The integration only supports one Z-Wave instance" + }, + "step": { + "user": { + "title": "Confirm set up" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json new file mode 100644 index 00000000000..f78b62828cb --- /dev/null +++ b/homeassistant/components/ozw/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "La integraci\u00f3n de MQTT no est\u00e1 configurada", + "one_instance_allowed": "La integraci\u00f3n solo admite una instancia de Z-Wave" + }, + "step": { + "user": { + "title": "Confirmar configuraci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/fi.json b/homeassistant/components/ozw/translations/fi.json new file mode 100644 index 00000000000..471c885a8b4 --- /dev/null +++ b/homeassistant/components/ozw/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Vahvista m\u00e4\u00e4ritt\u00e4minen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json new file mode 100644 index 00000000000..dbc609b93eb --- /dev/null +++ b/homeassistant/components/ozw/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", + "one_instance_allowed": "L'int\u00e9gration ne prend en charge qu'une seule instance Z-Wave" + }, + "step": { + "user": { + "title": "Confirmer la configuration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/it.json b/homeassistant/components/ozw/translations/it.json new file mode 100644 index 00000000000..0b76d09cf08 --- /dev/null +++ b/homeassistant/components/ozw/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "L'integrazione MQTT non \u00e8 impostata", + "one_instance_allowed": "L'integrazione supporta solo un'istanza Z-Wave" + }, + "step": { + "user": { + "title": "Confermare la configurazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ko.json b/homeassistant/components/ozw/translations/ko.json new file mode 100644 index 00000000000..2412e162c3c --- /dev/null +++ b/homeassistant/components/ozw/translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4", + "one_instance_allowed": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 Z-Wave \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + }, + "step": { + "user": { + "title": "\uc124\uc815 \ub0b4\uc6a9 \ud655\uc778\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json new file mode 100644 index 00000000000..053fe631133 --- /dev/null +++ b/homeassistant/components/ozw/translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "MQTT Integratioun ass net ageriicht", + "one_instance_allowed": "D'Integratioun \u00ebnnerst\u00ebtzt n\u00ebmmen 1 Z-Wave Instanz" + }, + "step": { + "user": { + "title": "Installatioun konfirm\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/nl.json b/homeassistant/components/ozw/translations/nl.json new file mode 100644 index 00000000000..e11b1ba1146 --- /dev/null +++ b/homeassistant/components/ozw/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "De [%%] integratie is niet ingesteld", + "one_instance_allowed": "De integratie ondersteunt, maar \u00e9\u00e9n Z-Wave-exemplaar" + }, + "step": { + "user": { + "title": "Bevestig de instelling" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json new file mode 100644 index 00000000000..1d4049978f5 --- /dev/null +++ b/homeassistant/components/ozw/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "MQTT-integrasjonen er ikke satt opp", + "one_instance_allowed": "Integrasjonen st\u00f8tter bare \u00e9n Z-Wave-forekomst" + }, + "step": { + "user": { + "title": "Bekreft oppsett" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json new file mode 100644 index 00000000000..3d57c3908a0 --- /dev/null +++ b/homeassistant/components/ozw/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "Integracja MQTT nie jest skonfigurowana.", + "one_instance_allowed": "Integracja obs\u0142uguje tylko jedn\u0105 instancj\u0119 Z-Wave." + }, + "step": { + "user": { + "title": "Potwierd\u017a konfiguracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json new file mode 100644 index 00000000000..ac968e9fdfa --- /dev/null +++ b/homeassistant/components/ozw/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", + "one_instance_allowed": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 Z-Wave." + }, + "step": { + "user": { + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json new file mode 100644 index 00000000000..43c78f930f2 --- /dev/null +++ b/homeassistant/components/ozw/translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "Integracija MQTT ni nastavljena", + "one_instance_allowed": "Integracija podpira samo en primerek Z-Wave" + }, + "step": { + "user": { + "title": "Potrdite nastavitev" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sv.json b/homeassistant/components/ozw/translations/sv.json new file mode 100644 index 00000000000..68b50437725 --- /dev/null +++ b/homeassistant/components/ozw/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Bekr\u00e4fta inst\u00e4llningen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hans.json b/homeassistant/components/ozw/translations/zh-Hans.json new file mode 100644 index 00000000000..e4beac109fd --- /dev/null +++ b/homeassistant/components/ozw/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "mqtt_required": "\u672a\u8bbe\u7f6e MQTT \u96c6\u6210" + }, + "step": { + "user": { + "title": "\u786e\u8ba4\u8bbe\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json new file mode 100644 index 00000000000..e9ad87042d2 --- /dev/null +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", + "one_instance_allowed": "\u6574\u5408\u50c5\u652f\u63f4\u4e00\u7d44 Z-Wave \u5be6\u4f8b" + }, + "step": { + "user": { + "title": "\u78ba\u8a8d\u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index 4a816252580..ddbd9f6670b 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -5,7 +5,7 @@ import logging from panacotta import PanasonicBD import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([PanasonicBluRay(conf[CONF_HOST], conf[CONF_NAME])]) -class PanasonicBluRay(MediaPlayerDevice): +class PanasonicBluRay(MediaPlayerEntity): """Representation of a Panasonic Blu-ray device.""" def __init__(self, ip, name): diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 60261712f4d..ebc1c20a1fa 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,13 +1,29 @@ """The Panasonic Viera integration.""" import asyncio +from functools import partial +import logging +from urllib.request import URLError +from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol +from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script -from .const import CONF_ON_ACTION, DEFAULT_NAME, DEFAULT_PORT, DOMAIN +from .const import ( + ATTR_REMOTE, + CONF_APP_ID, + CONF_ENCRYPTION_KEY, + CONF_ON_ACTION, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -28,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player"] +PLATFORMS = [MEDIA_PLAYER_DOMAIN] async def async_setup(hass, config): @@ -49,6 +65,27 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up Panasonic Viera from a config entry.""" + panasonic_viera_data = hass.data.setdefault(DOMAIN, {}) + + config = config_entry.data + + host = config[CONF_HOST] + port = config[CONF_PORT] + + on_action = config[CONF_ON_ACTION] + if on_action is not None: + on_action = Script(hass, on_action) + + params = {} + if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config: + params["app_id"] = config[CONF_APP_ID] + params["encryption_key"] = config[CONF_ENCRYPTION_KEY] + + remote = Remote(hass, host, port, on_action, **params) + await remote.async_create_remote_control(during_setup=True) + + panasonic_viera_data[config_entry.entry_id] = {ATTR_REMOTE: remote} + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, component) @@ -59,7 +96,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - return all( + unload_ok = all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(config_entry, component) @@ -67,3 +104,135 @@ async def async_unload_entry(hass, config_entry): ] ) ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +class Remote: + """The Remote class. It stores the TV properties and the remote control connection itself.""" + + def __init__( + self, hass, host, port, on_action=None, app_id=None, encryption_key=None, + ): + """Initialize the Remote class.""" + self._hass = hass + + self._host = host + self._port = port + + self._on_action = on_action + + self._app_id = app_id + self._encryption_key = encryption_key + + self.state = None + self.available = False + self.volume = 0 + self.muted = False + self.playing = True + + self._control = None + + async def async_create_remote_control(self, during_setup=False): + """Create remote control.""" + control_existed = self._control is not None + try: + params = {} + if self._app_id and self._encryption_key: + params["app_id"] = self._app_id + params["encryption_key"] = self._encryption_key + + self._control = await self._hass.async_add_executor_job( + partial(RemoteControl, self._host, self._port, **params) + ) + + self.state = STATE_ON + self.available = True + except (TimeoutError, URLError, SOAPError, OSError) as err: + if control_existed or during_setup: + _LOGGER.debug("Could not establish remote connection: %s", err) + + self._control = None + self.state = STATE_OFF + self.available = self._on_action is not None + except Exception as err: # pylint: disable=broad-except + if control_existed or during_setup: + _LOGGER.exception("An unknown error occurred: %s", err) + self._control = None + self.state = STATE_OFF + self.available = self._on_action is not None + + async def async_update(self): + """Update device data.""" + if self._control is None: + await self.async_create_remote_control() + return + + await self._handle_errors(self._update) + + def _update(self): + """Retrieve the latest data.""" + self.muted = self._control.get_mute() + self.volume = self._control.get_volume() / 100 + + self.state = STATE_ON + self.available = True + + async def async_send_key(self, key): + """Send a key to the TV and handle exceptions.""" + try: + key = getattr(Keys, key) + except (AttributeError, TypeError): + key = getattr(key, "value", key) + + await self._handle_errors(self._control.send_key, key) + + async def async_turn_on(self): + """Turn on the TV.""" + if self._on_action is not None: + await self._on_action.async_run() + self.state = STATE_ON + elif self.state != STATE_ON: + await self.async_send_key(Keys.power) + self.state = STATE_ON + + async def async_turn_off(self): + """Turn off the TV.""" + if self.state != STATE_OFF: + await self.async_send_key(Keys.power) + self.state = STATE_OFF + await self.async_update() + + async def async_set_mute(self, enable): + """Set mute based on 'enable'.""" + await self._handle_errors(self._control.set_mute, enable) + + async def async_set_volume(self, volume): + """Set volume level, range 0..1.""" + volume = int(volume * 100) + await self._handle_errors(self._control.set_volume, volume) + + async def async_play_media(self, media_type, media_id): + """Play media.""" + _LOGGER.debug("Play media: %s (%s)", media_id, media_type) + await self._handle_errors(self._control.open_webpage, media_id) + + async def _handle_errors(self, func, *args): + """Handle errors from func, set available and reconnect if needed.""" + try: + return await self._hass.async_add_executor_job(func, *args) + except EncryptionRequired: + _LOGGER.error( + "The connection couldn't be encrypted. Please reconfigure your TV" + ) + except (TimeoutError, URLError, SOAPError, OSError): + self.state = STATE_OFF + self.available = self._on_action is not None + await self.async_create_remote_control() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred: %s", err) + self.state = STATE_OFF + self.available = self._on_action is not None diff --git a/homeassistant/components/panasonic_viera/const.py b/homeassistant/components/panasonic_viera/const.py index 434d2d3d7c4..529de4ebe67 100644 --- a/homeassistant/components/panasonic_viera/const.py +++ b/homeassistant/components/panasonic_viera/const.py @@ -10,6 +10,8 @@ CONF_ENCRYPTION_KEY = "encryption_key" DEFAULT_NAME = "Panasonic Viera TV" DEFAULT_PORT = 55000 +ATTR_REMOTE = "remote" + ERROR_NOT_CONNECTED = "not_connected" ERROR_INVALID_PIN_CODE = "invalid_pin_code" diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index abfb10e11f3..7260b519bf4 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,11 +1,9 @@ -"""Support for interface with a Panasonic Viera TV.""" -from functools import partial +"""Media player support for Panasonic Viera TV.""" import logging -from urllib.request import URLError -from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError +from panasonic_viera import Keys -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_URL, SUPPORT_NEXT_TRACK, @@ -20,10 +18,9 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON -from homeassistant.helpers.script import Script +from homeassistant.const import CONF_NAME -from .const import CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION +from .const import ATTR_REMOTE, DOMAIN SUPPORT_VIERATV = ( SUPPORT_PAUSE @@ -47,42 +44,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = config_entry.data - host = config[CONF_HOST] - port = config[CONF_PORT] + remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE] name = config[CONF_NAME] - on_action = config[CONF_ON_ACTION] - if on_action is not None: - on_action = Script(hass, on_action) - - params = {} - if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config: - params["app_id"] = config[CONF_APP_ID] - params["encryption_key"] = config[CONF_ENCRYPTION_KEY] - - remote = Remote(hass, host, port, on_action, **params) - await remote.async_create_remote_control(during_setup=True) - - tv_device = PanasonicVieraTVDevice(remote, name) - + tv_device = PanasonicVieraTVEntity(remote, name) async_add_entities([tv_device]) -class PanasonicVieraTVDevice(MediaPlayerDevice): +class PanasonicVieraTVEntity(MediaPlayerEntity): """Representation of a Panasonic Viera TV.""" - def __init__( - self, remote, name, uuid=None, - ): - """Initialize the Panasonic device.""" - # Save a reference to the imported class + def __init__(self, remote, name, uuid=None): + """Initialize the entity.""" self._remote = remote self._name = name self._uuid = uuid @property - def unique_id(self) -> str: - """Return the unique ID of this Viera TV.""" + def unique_id(self): + """Return the unique ID of the device.""" return self._uuid @property @@ -97,7 +77,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): @property def available(self): - """Return if True the device is available.""" + """Return True if the device is available.""" return self._remote.available @property @@ -176,125 +156,8 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - await self._remote.async_play_media(media_type, media_id) - - -class Remote: - """The Remote class. It stores the TV properties and the remote control connection itself.""" - - def __init__( - self, hass, host, port, on_action=None, app_id=None, encryption_key=None, - ): - """Initialize the Remote class.""" - self._hass = hass - - self._host = host - self._port = port - - self._on_action = on_action - - self._app_id = app_id - self._encryption_key = encryption_key - - self.state = None - self.available = False - self.volume = 0 - self.muted = False - self.playing = True - - self._control = None - - async def async_create_remote_control(self, during_setup=False): - """Create remote control.""" - control_existed = self._control is not None - try: - params = {} - if self._app_id and self._encryption_key: - params["app_id"] = self._app_id - params["encryption_key"] = self._encryption_key - - self._control = await self._hass.async_add_executor_job( - partial(RemoteControl, self._host, self._port, **params) - ) - - self.state = STATE_ON - self.available = True - except (TimeoutError, URLError, SOAPError, OSError) as err: - if control_existed or during_setup: - _LOGGER.error("Could not establish remote connection: %s", err) - - self._control = None - self.state = STATE_OFF - self.available = self._on_action is not None - except Exception as err: # pylint: disable=broad-except - if control_existed or during_setup: - _LOGGER.exception("An unknown error occurred: %s", err) - self._control = None - self.state = STATE_OFF - self.available = self._on_action is not None - - async def async_update(self): - """Update device data.""" - if self._control is None: - await self.async_create_remote_control() - return - - await self._handle_errors(self._update) - - async def _update(self): - """Retrieve the latest data.""" - self.muted = self._control.get_mute() - self.volume = self._control.get_volume() / 100 - - self.state = STATE_ON - self.available = True - - async def async_send_key(self, key): - """Send a key to the TV and handle exceptions.""" - await self._handle_errors(self._control.send_key, key) - - async def async_turn_on(self): - """Turn on the TV.""" - if self._on_action is not None: - await self._on_action.async_run() - self.state = STATE_ON - elif self.state != STATE_ON: - await self.async_send_key(Keys.power) - self.state = STATE_ON - - async def async_turn_off(self): - """Turn off the TV.""" - if self.state != STATE_OFF: - await self.async_send_key(Keys.power) - self.state = STATE_OFF - await self.async_update() - - async def async_set_mute(self, enable): - """Set mute based on 'enable'.""" - await self._handle_errors(self._control.set_mute, enable) - - async def async_set_volume(self, volume): - """Set volume level, range 0..1.""" - volume = int(volume * 100) - await self._handle_errors(self._control.set_volume, volume) - - async def async_play_media(self, media_type, media_id): - """Play media.""" - _LOGGER.debug("Play media: %s (%s)", media_id, media_type) - if media_type != MEDIA_TYPE_URL: _LOGGER.warning("Unsupported media_type: %s", media_type) return - await self._handle_errors(self._control.open_webpage, media_id) - - async def _handle_errors(self, func, *args): - """Handle errors from func, set available and reconnect if needed.""" - try: - await self._hass.async_add_executor_job(func, *args) - except EncryptionRequired: - _LOGGER.error("The connection couldn't be encrypted") - except (TimeoutError, URLError, SOAPError, OSError): - self.state = STATE_OFF - self.available = self._on_action is not None - await self.async_create_remote_control() + await self._remote.async_play_media(media_type, media_id) diff --git a/homeassistant/components/panasonic_viera/strings.json b/homeassistant/components/panasonic_viera/strings.json index 9835d1b5d93..f3943fde71d 100644 --- a/homeassistant/components/panasonic_viera/strings.json +++ b/homeassistant/components/panasonic_viera/strings.json @@ -6,7 +6,7 @@ "title": "Setup your TV", "description": "Enter your Panasonic Viera TV's IP address", "data": { - "host": "IP address", + "host": "[%key:common::config_flow::data::ip%]", "name": "Name" } }, diff --git a/homeassistant/components/panasonic_viera/translations/es-419.json b/homeassistant/components/panasonic_viera/translations/es-419.json new file mode 100644 index 00000000000..d396fc58856 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Esta televisi\u00f3n Panasonic Viera ya est\u00e1 configurada.", + "not_connected": "Se perdi\u00f3 la conexi\u00f3n remota con su televisi\u00f3n Panasonic Viera. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", + "unknown": "Un error desconocido ocurri\u00f3. Consulte los registros para obtener m\u00e1s informaci\u00f3n." + }, + "error": { + "invalid_pin_code": "El c\u00f3digo PIN que ingres\u00f3 no es v\u00e1lido", + "not_connected": "No se pudo establecer una conexi\u00f3n remota con su televisi\u00f3n Panasonic Viera" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN" + }, + "description": "Ingrese el PIN que se muestra en su televisi\u00f3n", + "title": "Emparejamiento" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Ingrese la direcci\u00f3n IP de su Panasonic Viera TV", + "title": "Configurar su televisi\u00f3n" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/fi.json b/homeassistant/components/panasonic_viera/translations/fi.json new file mode 100644 index 00000000000..22e861af227 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/fi.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "unknown": "Tapahtui tuntematon virhe. Lis\u00e4tietoja on lokeissa." + }, + "error": { + "invalid_pin_code": "Antamasi PIN-koodi ei kelpaa" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN" + }, + "description": "Kirjoita televisiossa n\u00e4kyv\u00e4 PIN-koodi", + "title": "Paritus" + }, + "user": { + "data": { + "host": "IP-osoite", + "name": "Nimi" + }, + "description": "Anna Panasonic Viera TV:n IP-osoite", + "title": "Television m\u00e4\u00e4ritt\u00e4minen" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/fr.json b/homeassistant/components/panasonic_viera/translations/fr.json index dc9bfc3c920..4ee07e94ad4 100644 --- a/homeassistant/components/panasonic_viera/translations/fr.json +++ b/homeassistant/components/panasonic_viera/translations/fr.json @@ -7,13 +7,16 @@ "pairing": { "data": { "pin": "PIN" - } + }, + "description": "Entrer le code PIN affich\u00e9 sur votre t\u00e9l\u00e9viseur", + "title": "Appairage" }, "user": { "data": { "host": "Adresse IP", "name": "Nom" }, + "description": "Entrez l'adresse IP de votre t\u00e9l\u00e9viseur Panasonic Viera", "title": "Configurer votre t\u00e9l\u00e9viseur" } } diff --git a/homeassistant/components/panasonic_viera/translations/ko.json b/homeassistant/components/panasonic_viera/translations/ko.json new file mode 100644 index 00000000000..3e9c948c1c5 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/ko.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 Panasonic Viera TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "not_connected": "Panasonic Viera TV \uc640\uc758 \uc6d0\uaca9 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694" + }, + "error": { + "invalid_pin_code": "\uc785\ub825\ud55c PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_connected": "Panasonic Viera TV \uc5d0 \uc6d0\uaca9\uc73c\ub85c \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN" + }, + "description": "TV \uc5d0 \ud45c\uc2dc\ub41c PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "\ud398\uc5b4\ub9c1\ud558\uae30" + }, + "user": { + "data": { + "host": "IP \uc8fc\uc18c", + "name": "\uc774\ub984" + }, + "description": "Panasonic Viera TV \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "TV \uc124\uc815\ud558\uae30" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/no.json b/homeassistant/components/panasonic_viera/translations/no.json index b7f228552d9..039adbd2ad3 100644 --- a/homeassistant/components/panasonic_viera/translations/no.json +++ b/homeassistant/components/panasonic_viera/translations/no.json @@ -12,9 +12,9 @@ "step": { "pairing": { "data": { - "pin": "PIN" + "pin": "" }, - "description": "Skriv inn PIN-koden som vises p\u00e5 TV-en", + "description": "Angi PIN-koden som vises p\u00e5 TV-en", "title": "Sammenkobling" }, "user": { @@ -22,10 +22,10 @@ "host": "IP adresse", "name": "Navn" }, - "description": "Skriv inn IP-adressen til Panasonic Viera TV", + "description": "Fyll inn IP-adressen til Panasonic Viera TV", "title": "Sett opp TV-en din" } } }, - "title": "Panasonic Viera" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/pl.json b/homeassistant/components/panasonic_viera/translations/pl.json index 7a880c33265..5746a08acf3 100644 --- a/homeassistant/components/panasonic_viera/translations/pl.json +++ b/homeassistant/components/panasonic_viera/translations/pl.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "Ten telewizor Panasonic Viera jest ju\u017c skonfigurowany.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "not_connected": "Zdalne po\u0142\u0105czenie z telewizorem Panasonic Viera zosta\u0142o utracone. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji.", - "unknown": "Nieznany b\u0142\u0105d, sprawd\u017a logi aby uzyska\u0107 wi\u0119cej szczeg\u00f3\u0142\u00f3w" + "unknown": "Nieznany b\u0142\u0105d, sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej szczeg\u00f3\u0142\u00f3w" }, "error": { "invalid_pin_code": "Podany kod PIN jest nieprawid\u0142owy", diff --git a/homeassistant/components/panasonic_viera/translations/ru.json b/homeassistant/components/panasonic_viera/translations/ru.json index 549e8287a9f..0b840921df4 100644 --- a/homeassistant/components/panasonic_viera/translations/ru.json +++ b/homeassistant/components/panasonic_viera/translations/ru.json @@ -23,9 +23,9 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Panasonic Viera", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } } }, - "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Panasonic Viera" + "title": "Panasonic Viera TV" } \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/sl.json b/homeassistant/components/panasonic_viera/translations/sl.json new file mode 100644 index 00000000000..f81d28d6dfc --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/sl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Ta televizor Panasonic Viera je \u017ee konfiguriran.", + "not_connected": "Oddaljena povezava s televizorjem Panasonic Viera je bila prekinjena. Za ve\u010d informacij preverite dnevnike.", + "unknown": "Pri\u0161lo je do neznane napake. Za ve\u010d informacij preverite dnevnike." + }, + "error": { + "invalid_pin_code": "Koda PIN, ki ste jo vnesli, je neveljavna", + "not_connected": "Oddaljene povezave s TV Panasonic Viera ni bilo mogo\u010de vzpostaviti" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN" + }, + "description": "Vnesite kodo PIN, prikazano na televizorju", + "title": "Seznanjanje" + }, + "user": { + "data": { + "host": "IP naslov", + "name": "Ime" + }, + "description": "Vnesite IP naslov va\u0161ega Panasonic Viera TV", + "title": "Nastavite televizor" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json new file mode 100644 index 00000000000..f70336fae9f --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." + }, + "step": { + "user": { + "data": { + "host": "IP-adress", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 322765ac082..459e583c267 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -9,7 +9,7 @@ import signal import pexpect from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -72,12 +72,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([pandora]) -class PandoraMediaPlayer(MediaPlayerDevice): +class PandoraMediaPlayer(MediaPlayerEntity): """A media player that uses the Pianobar interface to Pandora.""" def __init__(self, name): """Initialize the Pandora device.""" - MediaPlayerDevice.__init__(self) self._name = name self._player_state = STATE_OFF self._station = "" diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py index 236fd47af73..8e14ea8ce69 100644 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -4,7 +4,7 @@ import logging from pcal9535a import PCAL9535A import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors, True) -class PCAL9535ABinarySensor(BinarySensorDevice): +class PCAL9535ABinarySensor(BinarySensorEntity): """Represent a binary sensor that uses PCAL9535A.""" def __init__(self, name, pin, pull_mode, invert_logic): diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py index 87c8ced1b0d..7b4cc919dbd 100644 --- a/homeassistant/components/pcal9535a/switch.py +++ b/homeassistant/components/pcal9535a/switch.py @@ -4,7 +4,7 @@ import logging from pcal9535a import PCAL9535A import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class PCAL9535ASwitch(SwitchDevice): +class PCAL9535ASwitch(SwitchEntity): """Representation of a PCAL9535A output pin.""" def __init__(self, name, pin, invert_logic): diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 5cd1f826629..0fcdd056b15 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -4,7 +4,7 @@ import logging from pencompy.pencompy import Pencompy 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_HOST, CONF_NAME, CONF_PORT from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs, True) -class PencomRelay(SwitchDevice): +class PencomRelay(SwitchEntity): """Representation of a pencom relay.""" def __init__(self, hub, board, addr, name): diff --git a/homeassistant/components/person/translations/it.json b/homeassistant/components/person/translations/it.json index 58e67c1fc6c..349ac076d14 100644 --- a/homeassistant/components/person/translations/it.json +++ b/homeassistant/components/person/translations/it.json @@ -1,7 +1,7 @@ { "state": { "_": { - "home": "A casa", + "home": "In casa", "not_home": "Fuori casa" } }, diff --git a/homeassistant/components/person/translations/no.json b/homeassistant/components/person/translations/no.json index 10115f789a6..6d380619114 100644 --- a/homeassistant/components/person/translations/no.json +++ b/homeassistant/components/person/translations/no.json @@ -1,3 +1,9 @@ { - "title": "Person" + "state": { + "_": { + "home": "Hjemme", + "not_home": "Borte" + } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index fe6d7edf804..f72aef2c464 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -5,7 +5,7 @@ import logging from haphilipsjs import PhilipsTV import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)]) -class PhilipsTVMediaPlayer(MediaPlayerDevice): +class PhilipsTVMediaPlayer(MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" def __init__(self, tv, name, on_script): diff --git a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py index 89f293d1e0d..4eb90101710 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py +++ b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py @@ -4,7 +4,7 @@ import logging from pi4ioe5v9xxxx import pi4ioe5v9xxxx # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors, True) -class Pi4ioe5v9BinarySensor(BinarySensorDevice): +class Pi4ioe5v9BinarySensor(BinarySensorEntity): """Represent a binary sensor that uses pi4ioe5v9xxxx IO expander in read mode.""" def __init__(self, name, pin, invert_logic): diff --git a/homeassistant/components/pi4ioe5v9xxxx/switch.py b/homeassistant/components/pi4ioe5v9xxxx/switch.py index a7c1e19074c..a147c7f310d 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/switch.py +++ b/homeassistant/components/pi4ioe5v9xxxx/switch.py @@ -4,7 +4,7 @@ import logging from pi4ioe5v9xxxx import pi4ioe5v9xxxx # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class Pi4ioe5v9Switch(SwitchDevice): +class Pi4ioe5v9Switch(SwitchEntity): """Representation of a pi4ioe5v9 IO expansion IO.""" def __init__(self, name, pin, invert_logic): diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 989841d1317..a0d6c5da6d1 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -6,6 +6,7 @@ from hole.exceptions import HoleError import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -13,14 +14,13 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.discovery import async_load_platform from homeassistant.util import Throttle from .const import ( CONF_LOCATION, - CONF_SLUG, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, @@ -34,31 +34,6 @@ from .const import ( SERVICE_ENABLE_ATTR_NAME, ) - -def ensure_unique_names_and_slugs(config): - """Ensure that each configuration dict contains a unique `name` value.""" - names = {} - slugs = {} - for conf in config: - if conf[CONF_NAME] not in names and conf[CONF_SLUG] not in slugs: - names[conf[CONF_NAME]] = conf[CONF_HOST] - slugs[conf[CONF_SLUG]] = conf[CONF_HOST] - else: - raise vol.Invalid( - f"Duplicate name '{conf[CONF_NAME]}' (or slug '{conf[CONF_SLUG]}') " - f"for '{conf[CONF_HOST]}' (already in use by " - f"'{names.get(conf[CONF_NAME], slugs[conf[CONF_SLUG]])}'). " - "Each configured Pi-hole must have a unique name." - ) - return config - - -def coerce_slug(config): - """Coerce the name of the Pi-Hole into a slug.""" - config[CONF_SLUG] = cv.slugify(config[CONF_NAME]) - return config - - LOGGER = logging.getLogger(__name__) PI_HOLE_SCHEMA = vol.Schema( @@ -71,16 +46,11 @@ PI_HOLE_SCHEMA = vol.Schema( vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }, - coerce_slug, ) ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - vol.All(cv.ensure_list, [PI_HOLE_SCHEMA], ensure_unique_names_and_slugs) - ) - }, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) @@ -88,81 +58,42 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the pi_hole integration.""" - def get_data(): - """Retrieve component data.""" - return hass.data[DOMAIN] - - def ensure_api_token(call_data): - """Ensure the Pi-Hole to be enabled/disabled has a api_token configured.""" - data = get_data() - if SERVICE_DISABLE_ATTR_NAME not in call_data: - for slug in data: - call_data[SERVICE_DISABLE_ATTR_NAME] = data[slug].name - ensure_api_token(call_data) - - call_data[SERVICE_DISABLE_ATTR_NAME] = None - else: - slug = cv.slugify(call_data[SERVICE_DISABLE_ATTR_NAME]) - - if (data[slug]).api.api_token is None: - raise vol.Invalid( - f"Pi-hole '{pi_hole.name}' must have an api_key " - "provided in configuration to be enabled." - ) - - return call_data - service_disable_schema = vol.Schema( vol.All( { vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( cv.time_period_str, cv.positive_timedelta ), - vol.Optional(SERVICE_DISABLE_ATTR_NAME): vol.In( - [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole" - ), + vol.Optional(SERVICE_DISABLE_ATTR_NAME): str, }, - ensure_api_token, ) ) - service_enable_schema = vol.Schema( - { - vol.Optional(SERVICE_ENABLE_ATTR_NAME): vol.In( - [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole" - ) - } - ) + service_enable_schema = vol.Schema({vol.Optional(SERVICE_ENABLE_ATTR_NAME): str}) hass.data[DOMAIN] = {} - for conf in config[DOMAIN]: - name = conf[CONF_NAME] - slug = conf[CONF_SLUG] - host = conf[CONF_HOST] - use_tls = conf[CONF_SSL] - verify_tls = conf[CONF_VERIFY_SSL] - location = conf[CONF_LOCATION] - api_key = conf.get(CONF_API_KEY) + # import + if DOMAIN in config: + for conf in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) - LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) - - session = async_get_clientsession(hass, verify_tls) - pi_hole = PiHoleData( - Hole( - host, - hass.loop, - session, - location=location, - tls=use_tls, - api_token=api_key, - ), - name, - ) - - await pi_hole.async_update() - - hass.data[DOMAIN][slug] = pi_hole + def get_pi_hole_from_name(name): + pi_hole = hass.data[DOMAIN].get(name) + if pi_hole is None: + LOGGER.error("Unknown Pi-hole name %s", name) + return None + if not pi_hole.api.api_token: + LOGGER.error( + "Pi-hole %s must have an api_key provided in configuration to be enabled", + name, + ) + return None + return pi_hole async def disable_service_handler(call): """Handle the service call to disable a single Pi-Hole or all configured Pi-Holes.""" @@ -171,8 +102,9 @@ async def async_setup(hass, config): async def do_disable(name): """Disable the named Pi-Hole.""" - slug = cv.slugify(name) - pi_hole = hass.data[DOMAIN][slug] + pi_hole = get_pi_hole_from_name(name) + if pi_hole is None: + return LOGGER.debug( "Disabling Pi-hole '%s' (%s) for %d seconds", @@ -185,8 +117,8 @@ async def async_setup(hass, config): if name is not None: await do_disable(name) else: - for pi_hole in hass.data[DOMAIN].values(): - await do_disable(pi_hole.name) + for name in hass.data[DOMAIN]: + await do_disable(name) async def enable_service_handler(call): """Handle the service call to enable a single Pi-Hole or all configured Pi-Holes.""" @@ -195,8 +127,9 @@ async def async_setup(hass, config): async def do_enable(name): """Enable the named Pi-Hole.""" - slug = cv.slugify(name) - pi_hole = hass.data[DOMAIN][slug] + pi_hole = get_pi_hole_from_name(name) + if pi_hole is None: + return LOGGER.debug("Enabling Pi-hole '%s' (%s)", name, pi_hole.api.host) await pi_hole.api.enable() @@ -204,8 +137,8 @@ async def async_setup(hass, config): if name is not None: await do_enable(name) else: - for pi_hole in hass.data[DOMAIN].values(): - await do_enable(pi_hole.name) + for name in hass.data[DOMAIN]: + await do_enable(name) hass.services.async_register( DOMAIN, SERVICE_DISABLE, disable_service_handler, schema=service_disable_schema @@ -215,11 +148,52 @@ async def async_setup(hass, config): DOMAIN, SERVICE_ENABLE, enable_service_handler, schema=service_enable_schema ) - hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) + return True + + +async def async_setup_entry(hass, entry): + """Set up Pi-hole entry.""" + name = entry.data[CONF_NAME] + host = entry.data[CONF_HOST] + use_tls = entry.data[CONF_SSL] + verify_tls = entry.data[CONF_VERIFY_SSL] + location = entry.data[CONF_LOCATION] + api_key = entry.data.get(CONF_API_KEY) + + LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + try: + session = async_get_clientsession(hass, verify_tls) + pi_hole = PiHoleData( + Hole( + host, + hass.loop, + session, + location=location, + tls=use_tls, + api_token=api_key, + ), + name, + ) + await pi_hole.async_update() + hass.data[DOMAIN][name] = pi_hole + except HoleError as ex: + LOGGER.warning("Failed to connect: %s", ex) + raise ConfigEntryNotReady + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) + ) return True +async def async_unload_entry(hass, entry): + """Unload pi-hole entry.""" + hass.data[DOMAIN].pop(entry.data[CONF_NAME]) + return await hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN) + + class PiHoleData: """Get the latest data and update the states.""" diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py new file mode 100644 index 00000000000..2b0ebfb7c16 --- /dev/null +++ b/homeassistant/components/pi_hole/config_flow.py @@ -0,0 +1,146 @@ +"""Config flow to configure the Pi-hole integration.""" +import logging + +from hole import Hole +from hole.exceptions import HoleError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.pi_hole.const import ( # pylint: disable=unused-import + CONF_LOCATION, + DEFAULT_LOCATION, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) + + +class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Pi-hole config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + return await self.async_step_init(user_input) + + async def async_step_import(self, user_input=None): + """Handle a flow initiated by import.""" + return await self.async_step_init(user_input, is_import=True) + + async def async_step_init(self, user_input, is_import=False): + """Handle init step of a flow.""" + errors = {} + + if user_input is not None: + host = ( + user_input[CONF_HOST] + if is_import + else f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" + ) + name = user_input[CONF_NAME] + location = user_input[CONF_LOCATION] + tls = user_input[CONF_SSL] + verify_tls = user_input[CONF_VERIFY_SSL] + api_token = user_input.get(CONF_API_KEY) + endpoint = f"{host}/{location}" + + if await self._async_endpoint_existed(endpoint): + return self.async_abort(reason="already_configured") + if await self._async_name_existed(name): + if is_import: + _LOGGER.error("Failed to import: name %s already existed", name) + return self.async_abort(reason="duplicated_name") + + try: + await self._async_try_connect( + host, location, tls, verify_tls, api_token + ) + return self.async_create_entry( + title=name, + data={ + CONF_HOST: host, + CONF_NAME: name, + CONF_LOCATION: location, + CONF_SSL: tls, + CONF_VERIFY_SSL: verify_tls, + CONF_API_KEY: api_token, + }, + ) + except HoleError as ex: + _LOGGER.debug("Connection failed: %s", ex) + if is_import: + _LOGGER.error("Failed to import: %s", ex) + return self.async_abort(reason="cannot_connect") + errors["base"] = "cannot_connect" + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST) or "" + ): str, + vol.Required( + CONF_PORT, default=user_input.get(CONF_PORT) or 80 + ): vol.Coerce(int), + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME) or DEFAULT_NAME + ): str, + vol.Required( + CONF_LOCATION, + default=user_input.get(CONF_LOCATION) or DEFAULT_LOCATION, + ): str, + vol.Optional( + CONF_API_KEY, default=user_input.get(CONF_API_KEY) or "" + ): str, + vol.Required( + CONF_SSL, default=user_input.get(CONF_SSL) or DEFAULT_SSL + ): bool, + vol.Required( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL) or DEFAULT_VERIFY_SSL, + ): bool, + } + ), + errors=errors, + ) + + async def _async_endpoint_existed(self, endpoint): + existing_endpoints = [ + f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" + for entry in self._async_current_entries() + ] + return endpoint in existing_endpoints + + async def _async_name_existed(self, name): + existing_names = [ + entry.data.get(CONF_NAME) for entry in self._async_current_entries() + ] + return name in existing_names + + async def _async_try_connect(self, host, location, tls, verify_tls, api_token): + session = async_get_clientsession(self.hass, verify_tls) + pi_hole = Hole( + host, + self.hass.loop, + session, + location=location, + tls=tls, + api_token=api_token, + ) + await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 94f687d9bfa..eec71ca441d 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -6,7 +6,6 @@ from homeassistant.const import UNIT_PERCENTAGE DOMAIN = "pi_hole" CONF_LOCATION = "location" -CONF_SLUG = "slug" DEFAULT_LOCATION = "admin" DEFAULT_METHOD = "GET" diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 1f4b46cc0d4..efe90bbf7e8 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -3,5 +3,6 @@ "name": "Pi-hole", "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": ["hole==0.5.1"], - "codeowners": ["@fabaff", "@johnluetke"] + "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], + "config_flow": true } diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index c01a0167e53..bbc42cdd8a5 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,6 +1,7 @@ """Support for getting statistical data from a Pi-hole system.""" import logging +from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import Entity from .const import ( @@ -13,29 +14,25 @@ from .const import ( LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the pi-hole sensor.""" - if discovery_info is None: - return - - sensors = [] - for pi_hole in hass.data[PIHOLE_DOMAIN].values(): - for sensor in [ - PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST - ]: - sensors.append(sensor) - + pi_hole = hass.data[PIHOLE_DOMAIN][entry.data[CONF_NAME]] + sensors = [ + PiHoleSensor(pi_hole, sensor_name, entry.entry_id) + for sensor_name in SENSOR_LIST + ] async_add_entities(sensors, True) class PiHoleSensor(Entity): """Representation of a Pi-hole sensor.""" - def __init__(self, pi_hole, sensor_name): + def __init__(self, pi_hole, sensor_name, server_unique_id): """Initialize a Pi-hole sensor.""" self.pi_hole = pi_hole self._name = pi_hole.name self._condition = sensor_name + self._server_unique_id = server_unique_id variable_info = SENSOR_DICT[sensor_name] self._condition_name = variable_info[0] @@ -48,6 +45,20 @@ class PiHoleSensor(Entity): """Return the name of the sensor.""" return f"{self._name} {self._condition_name}" + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._server_unique_id}/{self._condition_name}" + + @property + def device_info(self): + """Return the device information of the sensor.""" + return { + "identifiers": {(PIHOLE_DOMAIN, self._server_unique_id)}, + "name": self._name, + "manufacturer": "Pi-hole", + } + @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json new file mode 100644 index 00000000000..b155550844a --- /dev/null +++ b/homeassistant/components/pi_hole/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "name": "Name", + "api_key": "API Key (Optional)", + "ssl": "Use SSL", + "verify_ssl": "Verify SSL certificate" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "duplicated_name": "Name already existed" + } + } +} diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json new file mode 100644 index 00000000000..c913a799321 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json new file mode 100644 index 00000000000..ceefc0697cd --- /dev/null +++ b/homeassistant/components/pi_hole/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "duplicated_name": "Name already existed" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "api_key": "API Key (Optional)", + "host": "Host", + "name": "Name", + "port": "Port", + "ssl": "Use SSL", + "verify_ssl": "Verify SSL certificate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json new file mode 100644 index 00000000000..50cb5f98d16 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "duplicated_name": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json new file mode 100644 index 00000000000..9864e557439 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "duplicated_name": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470\uff08\u9078\u9805\uff09", + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py index 27bbb81d31f..1e6040f9d80 100644 --- a/homeassistant/components/piglow/light.py +++ b/homeassistant/components/piglow/light.py @@ -11,7 +11,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([PiglowLight(name)]) -class PiglowLight(Light): +class PiglowLight(LightEntity): """Representation of an Piglow Light.""" def __init__(self, name): diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index ae6d562725d..a53e575b875 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol from homeassistant.components import pilight -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( CONF_DISARM_AFTER_TRIGGER, CONF_NAME, @@ -72,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class PilightBinarySensor(BinarySensorDevice): +class PilightBinarySensor(BinarySensorEntity): """Representation of a binary sensor that can be updated using Pilight.""" def __init__(self, hass, name, variable, payload, on_value, off_value): @@ -121,7 +121,7 @@ class PilightBinarySensor(BinarySensorDevice): self.schedule_update_ha_state() -class PilightTriggerSensor(BinarySensorDevice): +class PilightTriggerSensor(BinarySensorEntity): """Representation of a binary sensor that can be updated using Pilight.""" def __init__( diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index 49ce3d9a124..12d175817d7 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -7,7 +7,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_LIGHTS import homeassistant.helpers.config_validation as cv @@ -40,7 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class PilightLight(PilightBaseDevice, Light): +class PilightLight(PilightBaseDevice, LightEntity): """Representation of a Pilight switch.""" def __init__(self, hass, name, config): diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index 0700b14e953..8fb2229f55c 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -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_SWITCHES import homeassistant.helpers.config_validation as cv @@ -27,5 +27,5 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class PilightSwitch(PilightBaseDevice, SwitchDevice): +class PilightSwitch(PilightBaseDevice, SwitchEntity): """Representation of a Pilight switch.""" diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 4d9a99c678e..a9c69f4ddad 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -7,7 +7,7 @@ import sys import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([PingBinarySensor(name, PingData(host, count))], True) -class PingBinarySensor(BinarySensorDevice): +class PingBinarySensor(BinarySensorEntity): """Representation of a Ping Binary sensor.""" def __init__(self, name, ping): diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index e93e6e5fb20..6316ea421e0 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -5,7 +5,7 @@ from pypjlink import MUTE_AUDIO, Projector from pypjlink.projector import ProjectorError import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -70,7 +70,7 @@ def format_input_source(input_source_name, input_source_number): return f"{input_source_name} {input_source_number}" -class PjLinkDevice(MediaPlayerDevice): +class PjLinkDevice(MediaPlayerEntity): """Representation of a PJLink device.""" def __init__(self, host, port, name, encoding, password): diff --git a/homeassistant/components/plaato/translations/ko.json b/homeassistant/components/plaato/translations/ko.json index 4cb94a81a72..10694fa470c 100644 --- a/homeassistant/components/plaato/translations/ko.json +++ b/homeassistant/components/plaato/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Plaato Airlock \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Plaato Airlock \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "description": "Plaato Airlock \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Plaato Webhook \uc124\uc815" + "title": "Plaato \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/plant/translations/no.json b/homeassistant/components/plant/translations/no.json index 0a08a5eaed4..e82299e36e9 100644 --- a/homeassistant/components/plant/translations/no.json +++ b/homeassistant/components/plant/translations/no.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "ok": "", + "problem": "" + } + }, "title": "Plantemonitor" } \ No newline at end of file diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index a314aba0ecd..e4d3751f661 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -4,6 +4,7 @@ import logging from aiohttp import web_response import plexapi.exceptions +from plexapi.gdm import GDM from plexauth import PlexAuth import requests.exceptions import voluptuous as vol @@ -11,22 +12,35 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import get_url from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, + AUTOMATIC_SETUP_STRING, CONF_CLIENT_IDENTIFIER, CONF_IGNORE_NEW_SHARED_USERS, + CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, + DEFAULT_PORT, + DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, + MANUAL_SETUP_STRING, PLEX_SERVER_CONFIG, SERVERS, X_PLEX_DEVICE_NAME, @@ -49,6 +63,18 @@ def configured_servers(hass): } +async def async_discover(hass): + """Scan for available Plex servers.""" + gdm = GDM() + await hass.async_add_executor_job(gdm.scan) + for server_data in gdm.entries: + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=server_data, + ) + + class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" @@ -68,14 +94,77 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.plexauth = None self.token = None self.client_id = None + self._manual = False - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None, errors=None): """Handle a flow initialized by the user.""" - return self.async_show_form(step_id="start_website_auth") + if user_input is not None: + return await self.async_step_plex_website_auth() + if self.show_advanced_options: + return await self.async_step_user_advanced(errors=errors) + return self.async_show_form(step_id="user", errors=errors) - async def async_step_start_website_auth(self, user_input=None): - """Show a form before starting external authentication.""" - return await self.async_step_plex_website_auth() + async def async_step_user_advanced(self, user_input=None, errors=None): + """Handle an advanced mode flow initialized by the user.""" + if user_input is not None: + if user_input.get("setup_method") == MANUAL_SETUP_STRING: + self._manual = True + return await self.async_step_manual_setup() + return await self.async_step_plex_website_auth() + + data_schema = vol.Schema( + { + vol.Required("setup_method", default=AUTOMATIC_SETUP_STRING): vol.In( + [AUTOMATIC_SETUP_STRING, MANUAL_SETUP_STRING] + ) + } + ) + return self.async_show_form( + step_id="user_advanced", data_schema=data_schema, errors=errors + ) + + async def async_step_manual_setup(self, user_input=None, errors=None): + """Begin manual configuration.""" + if user_input is not None and errors is None: + user_input.pop(CONF_URL, None) + host = user_input.get(CONF_HOST) + if host: + port = user_input[CONF_PORT] + prefix = "https" if user_input.get(CONF_SSL) else "http" + user_input[CONF_URL] = f"{prefix}://{host}:{port}" + elif CONF_TOKEN not in user_input: + return await self.async_step_manual_setup( + user_input=user_input, errors={"base": "host_or_token"} + ) + return await self.async_step_server_validate(user_input) + + previous_input = user_input or {} + + data_schema = vol.Schema( + { + vol.Optional( + CONF_HOST, + description={"suggested_value": previous_input.get(CONF_HOST)}, + ): str, + vol.Required( + CONF_PORT, default=previous_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Required( + CONF_SSL, default=previous_input.get(CONF_SSL, DEFAULT_SSL) + ): bool, + vol.Required( + CONF_VERIFY_SSL, + default=previous_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ): bool, + vol.Optional( + CONF_TOKEN, + description={"suggested_value": previous_input.get(CONF_TOKEN)}, + ): str, + } + ) + return self.async_show_form( + step_id="manual_setup", data_schema=data_schema, errors=errors + ) async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -95,13 +184,16 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") - errors["base"] = "faulty_credentials" + errors[CONF_TOKEN] = "faulty_credentials" + except requests.exceptions.SSLError as error: + _LOGGER.error("SSL certificate error: [%s]", error) + errors["base"] = "ssl_error" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): server_identifier = ( server_config.get(CONF_URL) or plex_server.server_choice or "Unknown" ) _LOGGER.error("Plex server could not be reached: %s", server_identifier) - errors["base"] = "not_found" + errors[CONF_HOST] = "not_found" except ServerNotSpecified as available_servers: if is_importing: @@ -119,7 +211,11 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if errors: if is_importing: return self.async_abort(reason="non-interactive") - return self.async_show_form(step_id="start_website_auth", errors=errors) + if self._manual: + return await self.async_step_manual_setup( + user_input=server_config, errors=errors + ) + return await self.async_step_user(errors=errors) server_id = plex_server.machine_identifier @@ -183,6 +279,19 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Imported Plex configuration") return await self.async_step_server_validate(import_config) + async def async_step_integration_discovery(self, discovery_info): + """Handle GDM discovery.""" + machine_identifier = discovery_info["data"]["Resource-Identifier"] + await self.async_set_unique_id(machine_identifier) + self._abort_if_unique_id_configured() + host = f"{discovery_info['from'][0]}:{discovery_info['data']['Port']}" + name = discovery_info["data"]["Name"] + self.context["title_placeholders"] = { # pylint: disable=no-member + "host": host, + "name": name, + } + return await self.async_step_user() + async def async_step_plex_website_auth(self): """Begin external auth flow on Plex website.""" self.hass.http.register_view(PlexAuthorizationCallbackView) @@ -197,7 +306,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session = async_get_clientsession(self.hass) self.plexauth = PlexAuth(payload, session) await self.plexauth.initiate_auth() - forward_url = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" + forward_url = f"{get_url(self.hass)}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" auth_url = self.plexauth.auth_url(forward_url) return self.async_external_step(step_id="obtain_token", url=auth_url) @@ -245,6 +354,9 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): self.options[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = user_input[ CONF_IGNORE_NEW_SHARED_USERS ] + self.options[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = user_input[ + CONF_IGNORE_PLEX_WEB_CLIENTS + ] account_data = { user: {"enabled": bool(user in user_input[CONF_MONITORED_USERS])} @@ -289,6 +401,10 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): CONF_IGNORE_NEW_SHARED_USERS, default=plex_server.option_ignore_new_shared_users, ): bool, + vol.Required( + CONF_IGNORE_PLEX_WEB_CLIENTS, + default=plex_server.option_ignore_plexweb_clients, + ): bool, } ), ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 555454e2205..e8bcfb42ca6 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -30,6 +30,7 @@ CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" CONF_IGNORE_NEW_SHARED_USERS = "ignore_new_shared_users" +CONF_IGNORE_PLEX_WEB_CLIENTS = "ignore_plex_web_clients" CONF_MONITORED_USERS = "monitored_users" AUTH_CALLBACK_PATH = "/auth/plex/callback" @@ -39,3 +40,6 @@ X_PLEX_DEVICE_NAME = "Home Assistant" X_PLEX_PLATFORM = "Home Assistant" X_PLEX_PRODUCT = "Home Assistant" X_PLEX_VERSION = __version__ + +AUTOMATIC_SETUP_STRING = "Obtain a new token from plex.tv" +MANUAL_SETUP_STRING = "Configure Plex server manually" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index a458a3585b4..e48a37a77d5 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -3,7 +3,7 @@ "name": "Plex Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", - "requirements": ["plexapi==3.4.0", "plexauth==0.0.5", "plexwebsocket==0.0.7"], + "requirements": ["plexapi==3.6.0", "plexauth==0.0.5", "plexwebsocket==0.0.8"], "dependencies": ["http"], "codeowners": ["@jjlawren"] } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 79d1339bde1..b19f687482c 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -5,7 +5,7 @@ import logging import plexapi.exceptions import requests.exceptions -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerDevice +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, @@ -88,7 +88,7 @@ def _async_add_entities( async_add_entities(entities, True) -class PlexMediaPlayer(MediaPlayerDevice): +class PlexMediaPlayer(MediaPlayerEntity): """Representation of a Plex device.""" def __init__(self, plex_server, device, session=None): diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index f4d5d24049c..30761f11bdd 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -85,11 +85,16 @@ class PlexSensor(Entity): if sess.TYPE in ["clip", "episode"]: # example: # "Supernatural (2005) - s01e13 - Route 666" + + def sync_io_attributes(session): + return (session.show(), session.seasonEpisode) + + show, season_episode = await self.hass.async_add_executor_job( + sync_io_attributes, sess + ) season_title = sess.grandparentTitle - show = await self.hass.async_add_executor_job(sess.show) if show.year is not None: season_title += f" ({show.year!s})" - season_episode = sess.seasonEpisode episode_title = sess.title now_playing_title = ( f"{season_title} - {season_episode} - {episode_title}" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index ca5e4756cf2..a1f5af321f3 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -3,6 +3,7 @@ import logging import ssl from urllib.parse import urlparse +from plexapi.exceptions import Unauthorized import plexapi.myplex import plexapi.playqueue import plexapi.server @@ -18,6 +19,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_CLIENT_IDENTIFIER, CONF_IGNORE_NEW_SHARED_USERS, + CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, CONF_SERVER, CONF_USE_EPISODE_ART, @@ -49,6 +51,7 @@ class PlexServer: """Initialize a Plex server instance.""" self.hass = hass self._plex_server = None + self._created_clients = set() self._known_clients = set() self._known_idle = set() self._url = server_config.get(CONF_URL) @@ -131,26 +134,31 @@ class PlexServer: ) _update_plexdirect_hostname() config_entry_update_needed = True + else: + raise else: raise else: _connect_with_token() - self._accounts = [ - account.name - for account in self._plex_server.systemAccounts() - if account.name - ] - _LOGGER.debug("Linked accounts: %s", self.accounts) + try: + system_accounts = self._plex_server.systemAccounts() + except Unauthorized: + _LOGGER.warning( + "Plex account has limited permissions, shared account filtering will not be available." + ) + else: + self._accounts = [ + account.name for account in system_accounts if account.name + ] + _LOGGER.debug("Linked accounts: %s", self.accounts) - owner_account = [ - account.name - for account in self._plex_server.systemAccounts() - if account.accountID == 1 - ] - if owner_account: - self._owner_username = owner_account[0] - _LOGGER.debug("Server owner found: '%s'", self._owner_username) + owner_account = [ + account.name for account in system_accounts if account.accountID == 1 + ] + if owner_account: + self._owner_username = owner_account[0] + _LOGGER.debug("Server owner found: '%s'", self._owner_username) self._version = self._plex_server.version @@ -207,42 +215,60 @@ class PlexServer: ) return - for device in devices: + def process_device(source, device): self._known_idle.discard(device.machineIdentifier) - available_clients[device.machineIdentifier] = {"device": device} + available_clients.setdefault(device.machineIdentifier, {"device": device}) - if device.machineIdentifier not in self._known_clients: + if device.machineIdentifier not in ignored_clients: + if self.option_ignore_plexweb_clients and device.product == "Plex Web": + ignored_clients.add(device.machineIdentifier) + if device.machineIdentifier not in self._known_clients: + _LOGGER.debug( + "Ignoring %s %s: %s", + "Plex Web", + source, + device.machineIdentifier, + ) + return + + if ( + device.machineIdentifier not in self._created_clients + and device.machineIdentifier not in ignored_clients + and device.machineIdentifier not in new_clients + ): new_clients.add(device.machineIdentifier) - _LOGGER.debug("New device: %s", device.machineIdentifier) + _LOGGER.debug( + "New %s %s: %s", device.product, source, device.machineIdentifier + ) + + for device in devices: + process_device("device", device) for session in sessions: if session.TYPE == "photo": _LOGGER.debug("Photo session detected, skipping: %s", session) continue + session_username = session.usernames[0] for player in session.players: if session_username and session_username not in monitored_users: ignored_clients.add(player.machineIdentifier) _LOGGER.debug( - "Ignoring Plex client owned by '%s'", session_username + "Ignoring %s client owned by '%s'", + player.product, + session_username, ) continue - self._known_idle.discard(player.machineIdentifier) - available_clients.setdefault( - player.machineIdentifier, {"device": player} - ) + process_device("session", player) available_clients[player.machineIdentifier]["session"] = session - if player.machineIdentifier not in self._known_clients: - new_clients.add(player.machineIdentifier) - _LOGGER.debug("New session: %s", player.machineIdentifier) - new_entity_configs = [] for client_id, client_data in available_clients.items(): if client_id in ignored_clients: continue if client_id in new_clients: new_entity_configs.append(client_data) + self._created_clients.add(client_id) else: self.async_refresh_entity( client_id, client_data["device"], client_data.get("session") @@ -320,6 +346,11 @@ class PlexServer: """Return dict of monitored users option.""" return self.options[MP_DOMAIN].get(CONF_MONITORED_USERS, {}) + @property + def option_ignore_plexweb_clients(self): + """Return ignore_plex_web_clients option.""" + return self.options[MP_DOMAIN].get(CONF_IGNORE_PLEX_WEB_CLIENTS, False) + @property def library(self): """Return library attribute from server object.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 962e8d35225..09dbc9a8ecf 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -1,20 +1,41 @@ { "config": { + "flow_title": "{name} ({host})", "step": { + "user": { + "title": "Plex Media Server", + "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server." + }, + "user_advanced": { + "title": "Plex Media Server", + "data": { + "setup_method": "Setup method" + } + }, + "manual_setup": { + "title": "Manual Plex Configuration", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "Use SSL", + "verify_ssl": "Verify SSL certificate", + "token": "Token (Optional)" + } + }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", - "data": { "server": "Server" } - }, - "start_website_auth": { - "title": "Connect Plex server", - "description": "Continue to authorize at plex.tv." + "data": { + "server": "Server" + } } }, "error": { - "faulty_credentials": "Authorization failed", - "no_servers": "No servers linked to account", - "not_found": "Plex server not found" + "faulty_credentials": "Authorization failed, verify Token", + "host_or_token": "Must provide at least one of Host or Token", + "no_servers": "No servers linked to Plex account", + "not_found": "Plex server not found", + "ssl_error": "SSL certificate issue" }, "abort": { "all_configured": "All linked servers already configured", @@ -33,9 +54,10 @@ "data": { "use_episode_art": "Use episode art", "ignore_new_shared_users": "Ignore new managed/shared users", - "monitored_users": "Monitored users" + "monitored_users": "Monitored users", + "ignore_plex_web_clients": "Ignore Plex Web clients" } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/ca.json b/homeassistant/components/plex/translations/ca.json index 250f1a3b2f2..72cc2d97a30 100644 --- a/homeassistant/components/plex/translations/ca.json +++ b/homeassistant/components/plex/translations/ca.json @@ -3,18 +3,30 @@ "abort": { "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", - "already_in_progress": "S\u2019est\u00e0 configurant Plex", + "already_in_progress": "S'est\u00e0 configurant Plex", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", "non-interactive": "Importaci\u00f3 no interactiva", - "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", + "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del token.", "unknown": "Ha fallat per motiu desconegut" }, "error": { "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", + "host_or_token": "Has de proporcionar almenys o un amfitri\u00f3 (host) o un token", "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", - "not_found": "No s'ha trobat el servidor Plex" + "not_found": "No s'ha trobat el servidor Plex", + "ssl_error": "Problema amb el certificat SSL" }, "step": { + "manual_setup": { + "data": { + "host": "Amfitri\u00f3 (opcional si es proporciona un token)", + "port": "Port", + "ssl": "Utilitza SSL", + "token": "Token (opcional)", + "verify_ssl": "Verifica el certificat SSL" + }, + "title": "Configuraci\u00f3 manual de Plex" + }, "select_server": { "data": { "server": "Servidor" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Continua l'autoritzaci\u00f3 a plex.tv.", "title": "Connexi\u00f3 amb el servidor Plex" + }, + "user": { + "description": "V\u00e9s a [plex.tv](https://plex.tv) per enlla\u00e7ar un servidor Plex.", + "title": "Servidor Multim\u00e8dia Plex" + }, + "user_advanced": { + "data": { + "setup_method": "M\u00e8tode de configuraci\u00f3" + }, + "title": "Servidor Multim\u00e8dia Plex" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignora els nous usuaris gestionats/compartits", + "ignore_plex_web_clients": "Ignora els clients de Plex Web", "monitored_users": "Usuaris monitoritzats", "use_episode_art": "Utilitza imatges de l'episodi" }, diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index e8aea9921c7..3d002cc299c 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -10,11 +10,23 @@ "unknown": "Aus unbekanntem Grund fehlgeschlagen" }, "error": { - "faulty_credentials": "Autorisation fehlgeschlagen", - "no_servers": "Keine Server sind mit dem Konto verbunden", - "not_found": "Plex-Server nicht gefunden" + "faulty_credentials": "Autorisierung fehlgeschlagen, Token \u00fcberpr\u00fcfen", + "host_or_token": "Es muss mindestens ein Host oder ein Token bereitgestellt werden", + "no_servers": "Keine Server mit Plex-Konto verbunden", + "not_found": "Plex-Server nicht gefunden", + "ssl_error": "SSL-Zertifikatsproblem" }, "step": { + "manual_setup": { + "data": { + "host": "Host (Optional, wenn Token bereitgestellt)", + "port": "Port", + "ssl": "SSL verwenden", + "token": "Token (optional)", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "title": "Manuelle Plex-Konfiguration" + }, "select_server": { "data": { "server": "Server" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Weiter zur Autorisierung unter plex.tv.", "title": "Plex Server verbinden" + }, + "user": { + "description": "Gehen Sie zu [plex.tv] (https://plex.tv), um einen Plex-Server zu verbinden", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Einrichtungsmethode" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignorieren neuer verwalteter/freigegebener Benutzer", + "ignore_plex_web_clients": "Plex-Webclients ignorieren", "monitored_users": "\u00dcberwachte Benutzer", "use_episode_art": "Episode-Bilder verwenden" }, diff --git a/homeassistant/components/plex/translations/en.json b/homeassistant/components/plex/translations/en.json index 731e95915ca..d0c9a0d4e32 100644 --- a/homeassistant/components/plex/translations/en.json +++ b/homeassistant/components/plex/translations/en.json @@ -10,11 +10,24 @@ "unknown": "Failed for unknown reason" }, "error": { - "faulty_credentials": "Authorization failed", - "no_servers": "No servers linked to account", - "not_found": "Plex server not found" + "faulty_credentials": "Authorization failed, verify Token", + "host_or_token": "Must provide at least one of Host or Token", + "no_servers": "No servers linked to Plex account", + "not_found": "Plex server not found", + "ssl_error": "SSL certificate issue" }, + "flow_title": "{name} ({host})", "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Use SSL", + "token": "Token (Optional)", + "verify_ssl": "Verify SSL certificate" + }, + "title": "Manual Plex Configuration" + }, "select_server": { "data": { "server": "Server" @@ -25,6 +38,16 @@ "start_website_auth": { "description": "Continue to authorize at plex.tv.", "title": "Connect Plex server" + }, + "user": { + "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Setup method" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +56,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignore new managed/shared users", + "ignore_plex_web_clients": "Ignore Plex Web clients", "monitored_users": "Monitored users", "use_episode_art": "Use episode art" }, diff --git a/homeassistant/components/plex/translations/es-419.json b/homeassistant/components/plex/translations/es-419.json index 8fa797aec89..72cd3deefbb 100644 --- a/homeassistant/components/plex/translations/es-419.json +++ b/homeassistant/components/plex/translations/es-419.json @@ -5,6 +5,7 @@ "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Se agot\u00f3 el tiempo de espera para obtener el token", "unknown": "Fall\u00f3 por razones desconocidas" }, @@ -20,12 +21,21 @@ }, "description": "M\u00faltiples servidores disponibles, seleccione uno:", "title": "Seleccionar servidor Plex" + }, + "start_website_auth": { + "description": "Continuar autorizando en plex.tv.", + "title": "Conectar a servidor Plex" } } }, "options": { "step": { "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", + "monitored_users": "Usuarios monitoreados", + "use_episode_art": "Usa el arte del episodio" + }, "description": "Opciones para reproductores multimedia Plex" } } diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index b26c6a4b381..6959c59deca 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -10,11 +10,23 @@ "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { - "faulty_credentials": "Error en la autorizaci\u00f3n", - "no_servers": "No hay servidores vinculados a la cuenta", - "not_found": "No se ha encontrado el servidor Plex" + "faulty_credentials": "La autorizaci\u00f3n fall\u00f3, verifica el token", + "host_or_token": "Debes proporcionar al menos uno de Host o Token", + "no_servers": "No hay servidores vinculados a la cuenta Plex", + "not_found": "No se ha encontrado el servidor Plex", + "ssl_error": "Problema con el certificado SSL" }, "step": { + "manual_setup": { + "data": { + "host": "Host (Opcional si se proporciona Token)", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (Opcional)", + "verify_ssl": "Verificar certificado SSL" + }, + "title": "Configuraci\u00f3n Manual de Plex" + }, "select_server": { "data": { "server": "Servidor" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Contin\u00fae en plex.tv para autorizar", "title": "Conectar servidor Plex" + }, + "user": { + "description": "Continuar hacia [plex.tv](https://plex.tv) para vincular un servidor Plex.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "M\u00e9todo de configuraci\u00f3n" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", + "ignore_plex_web_clients": "Ignorar clientes web de Plex", "monitored_users": "Usuarios monitorizados", "use_episode_art": "Usar el arte de episodios" }, diff --git a/homeassistant/components/plex/translations/fi.json b/homeassistant/components/plex/translations/fi.json new file mode 100644 index 00000000000..2c5872fe85a --- /dev/null +++ b/homeassistant/components/plex/translations/fi.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "manual_setup": { + "data": { + "host": "Palvelin (valinnainen, jos token on annettu)", + "port": "Portti", + "ssl": "K\u00e4yt\u00e4 SSL:\u00e4\u00e4", + "token": "Token (valinnainen)" + }, + "title": "Manuaalinen Plex-konfigurointi" + }, + "user": { + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Asennusmenetelm\u00e4" + }, + "title": "Plex-mediapalvelin" + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "ignore_plex_web_clients": "Ohita Plex web-asiakasohjelmat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/fr.json b/homeassistant/components/plex/translations/fr.json index 6950a0de170..7ec9b29c7ff 100644 --- a/homeassistant/components/plex/translations/fr.json +++ b/homeassistant/components/plex/translations/fr.json @@ -12,9 +12,20 @@ "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", "no_servers": "Aucun serveur li\u00e9 au compte", - "not_found": "Serveur Plex introuvable" + "not_found": "Serveur Plex introuvable", + "ssl_error": "Probl\u00e8me de certificat SSL" }, "step": { + "manual_setup": { + "data": { + "host": "H\u00f4te (facultatif si un jeton est fourni)", + "port": "Port", + "ssl": "Utiliser SSL", + "token": "Jeton (facultatif)", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Configuration manuelle de Plex" + }, "select_server": { "data": { "server": "Serveur" @@ -25,6 +36,15 @@ "start_website_auth": { "description": "Continuer d'autoriser sur plex.tv.", "title": "Connecter un serveur Plex" + }, + "user": { + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "M\u00e9thode de configuration" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +53,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignorer les nouveaux utilisateurs g\u00e9r\u00e9s/partag\u00e9s", + "ignore_plex_web_clients": "Ignorer les clients Web Plex", "monitored_users": "Utilisateurs surveill\u00e9s", "use_episode_art": "Utiliser l'art de l'\u00e9pisode" }, diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index 5296b208e78..839eff780de 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -15,6 +15,12 @@ "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" }, "step": { + "manual_setup": { + "data": { + "port": "Port", + "ssl": "Haszn\u00e1ljon SSL-t" + } + }, "select_server": { "data": { "server": "szerver" @@ -25,6 +31,12 @@ "start_website_auth": { "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen.", "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" + }, + "user": { + "title": "Plex Media Server" + }, + "user_advanced": { + "title": "Plex Media Server" } } }, diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json index 503cfcbd110..d7bd1060985 100644 --- a/homeassistant/components/plex/translations/it.json +++ b/homeassistant/components/plex/translations/it.json @@ -10,11 +10,23 @@ "unknown": "Non riuscito per motivo sconosciuto" }, "error": { - "faulty_credentials": "Autorizzazione non riuscita", - "no_servers": "Nessun server collegato all'account", - "not_found": "Server Plex non trovato" + "faulty_credentials": "Autorizzazione non riuscita, verificare il Token", + "host_or_token": "Si deve fornire almeno un Host o un Token", + "no_servers": "Nessun server collegato all'account Plex", + "not_found": "Server Plex non trovato", + "ssl_error": "Problema con il certificato SSL" }, "step": { + "manual_setup": { + "data": { + "host": "Host (opzionale se si \u00e8 fornito un Token)", + "port": "Porta", + "ssl": "Utilizzare SSL", + "token": "Token (opzionale)", + "verify_ssl": "Verificare il certificato SSL" + }, + "title": "Configurazione manuale Plex" + }, "select_server": { "data": { "server": "Server" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Continuare ad autorizzare su plex.tv.", "title": "Collegare il server Plex" + }, + "user": { + "description": "Continuare su [plex.tv](https://plex.tv) per collegare un server Plex.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Metodo di impostazione" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignora nuovi utenti gestiti/condivisi", + "ignore_plex_web_clients": "Ignora i client Web Plex", "monitored_users": "Utenti monitorati", "use_episode_art": "Usa la grafica dell'episodio" }, diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json index 1c8e51f9f1c..4443e3bc218 100644 --- a/homeassistant/components/plex/translations/ko.json +++ b/homeassistant/components/plex/translations/ko.json @@ -10,21 +10,43 @@ "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", - "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud1a0\ud070\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694", + "host_or_token": "\ud638\uc2a4\ud2b8 \ub610\ub294 \ud1a0\ud070 \uc911 \ud558\ub098 \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4", + "no_servers": "Plex \uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "ssl_error": "SSL \uc778\uc99d\uc11c \uac80\uc99d" }, "step": { + "manual_setup": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc0ac\uc6a9", + "token": "\ud1a0\ud070 (\uc120\ud0dd \uc0ac\ud56d)", + "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + }, + "title": "Plex \uc9c1\uc811 \uad6c\uc131\ud558\uae30" + }, "select_server": { "data": { "server": "\uc11c\ubc84" }, "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", - "title": "Plex \uc11c\ubc84 \uc120\ud0dd" + "title": "Plex \uc11c\ubc84 \uc120\ud0dd\ud558\uae30" }, "start_website_auth": { "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", - "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0\ud558\uae30" + }, + "user": { + "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv) \ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", + "title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84" + }, + "user_advanced": { + "data": { + "setup_method": "\uc124\uc815 \ubc29\ubc95" + }, + "title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "\uc0c8\ub85c\uc6b4 \uad00\ub9ac/\uacf5\uc720 \uc0ac\uc6a9\uc790 \ubb34\uc2dc", + "ignore_plex_web_clients": "Plex \uc6f9 \ud074\ub77c\uc774\uc5b8\ud2b8 \ubb34\uc2dc", "monitored_users": "\ubaa8\ub2c8\ud130\ub9c1\ub418\ub294 \uc0ac\uc6a9\uc790", "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9" }, diff --git a/homeassistant/components/plex/translations/lb.json b/homeassistant/components/plex/translations/lb.json index 563d2282058..c325389295c 100644 --- a/homeassistant/components/plex/translations/lb.json +++ b/homeassistant/components/plex/translations/lb.json @@ -11,10 +11,22 @@ }, "error": { "faulty_credentials": "Feeler beider Autorisatioun", + "host_or_token": "Op manst een Apparat oder Jeton muss ugi sinn.", "no_servers": "Kee Server as mam Kont verbonnen", - "not_found": "Kee Plex Server fonnt" + "not_found": "Kee Plex Server fonnt", + "ssl_error": "SSL Zertifikat Problem" }, "step": { + "manual_setup": { + "data": { + "host": "Apparat (Optionell)", + "port": "Port", + "ssl": "SSL benotzen", + "token": "Jeton (Optionell)", + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + }, + "title": "Manuell Plex Konfiguratioun" + }, "select_server": { "data": { "server": "Server" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", "title": "Plex Server verbannen" + }, + "user": { + "description": "Verbann dech mat [plex.tv](https://pley.tv) fir ee Plex Server ze verlinken.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Setup Method" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Nei verwalt / gedeelt Benotzer ignor\u00e9ieren", + "ignore_plex_web_clients": "Plex Web Cliente ignor\u00e9ieren", "monitored_users": "Iwwerwaachte Benotzer", "use_episode_art": "Benotz Biller vun der Episode" }, diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index f3743bd0ffb..19a4db4b8a6 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -10,11 +10,23 @@ "unknown": "Mislyktes av ukjent \u00e5rsak" }, "error": { - "faulty_credentials": "Autorisasjonen mislyktes", - "no_servers": "Ingen servere koblet til kontoen", - "not_found": "Plex-server ikke funnet" + "faulty_credentials": "Autorisasjonen mislyktes, bekreft token", + "host_or_token": "M\u00e5 oppgi minst en av Host eller Token", + "no_servers": "Ingen servere koblet til Plex-konto", + "not_found": "Plex-server ikke funnet", + "ssl_error": "Problem med SSL-sertifikat" }, "step": { + "manual_setup": { + "data": { + "host": "Host (valgfritt hvis token f\u00f8lger med)", + "port": "Port", + "ssl": "Bruk SSL", + "token": "Token (valgfritt)", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "title": "Manuell Plex-konfigurasjon" + }, "select_server": { "data": { "server": "" @@ -23,8 +35,18 @@ "title": "Velg Plex-server" }, "start_website_auth": { - "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", + "description": "Fortsett \u00e5 godkjenne p\u00e5 plex.tv", "title": "Koble til Plex-server" + }, + "user": { + "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Oppsettmetode" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +55,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignorer nye administrerte/delte brukere", + "ignore_plex_web_clients": "Ignorer Plex Web-klienter", "monitored_users": "Overv\u00e5kede brukere", "use_episode_art": "Bruk episode bilde" }, diff --git a/homeassistant/components/plex/translations/pl.json b/homeassistant/components/plex/translations/pl.json index 53f80db934d..6b61382458b 100644 --- a/homeassistant/components/plex/translations/pl.json +++ b/homeassistant/components/plex/translations/pl.json @@ -11,10 +11,22 @@ }, "error": { "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", + "host_or_token": "Nalezy wprowadzi\u0107 przynajmniej jeden host lub token.", "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", - "not_found": "Nie znaleziono serwera Plex" + "not_found": "Nie znaleziono serwera Plex", + "ssl_error": "Problem z certyfikatem SSL." }, "step": { + "manual_setup": { + "data": { + "host": "[%key_id:common::config_flow::data::host%] (opcjonalnie, je\u015bli wprowadzono token)", + "port": "[%key_id:common::config_flow::data::port%]", + "ssl": "U\u017cyj SSL", + "token": "[%key_id:common::config_flow::data::access_token%] (opcjonalnie)", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "title": "Manualna konfiguracja Plex" + }, "select_server": { "data": { "server": "Serwer" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.", "title": "Po\u0142\u0105cz z serwerem Plex" + }, + "user": { + "description": "Przejd\u017a do [plex.tv](https://plex.tv), aby po\u0142\u0105czy\u0107 serwer Plex.", + "title": "Serwer medi\u00f3w Plex" + }, + "user_advanced": { + "data": { + "setup_method": "Metoda konfiguracji" + }, + "title": "Serwer medi\u00f3w Plex" } } }, diff --git a/homeassistant/components/plex/translations/ru.json b/homeassistant/components/plex/translations/ru.json index 44de2e39554..8c08d3df703 100644 --- a/homeassistant/components/plex/translations/ru.json +++ b/homeassistant/components/plex/translations/ru.json @@ -10,11 +10,24 @@ "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435." }, "error": { - "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0422\u043e\u043a\u0435\u043d.", + "host_or_token": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0425\u043e\u0441\u0442 \u0438\u043b\u0438 \u0422\u043e\u043a\u0435\u043d.", "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", - "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", + "ssl_error": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0441 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u043c." }, + "flow_title": "{name} ({host})", "step": { + "manual_setup": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL", + "token": "\u0422\u043e\u043a\u0435\u043d (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "title": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Plex" + }, "select_server": { "data": { "server": "\u0421\u0435\u0440\u0432\u0435\u0440" @@ -25,6 +38,16 @@ "start_website_auth": { "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", "title": "Plex" + }, + "user": { + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [plex.tv](https://plex.tv), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u043a Home Assistant.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "title": "Plex Media Server" } } }, @@ -33,6 +56,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0445 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445/\u043e\u0431\u0449\u0438\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439", + "ignore_plex_web_clients": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0435\u0431-\u043a\u043b\u0438\u0435\u043d\u0442\u044b Plex", "monitored_users": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438", "use_episode_art": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u044d\u043f\u0438\u0437\u043e\u0434\u043e\u0432" }, diff --git a/homeassistant/components/plex/translations/sl.json b/homeassistant/components/plex/translations/sl.json index 3ae1885e76e..0d5fb88aae2 100644 --- a/homeassistant/components/plex/translations/sl.json +++ b/homeassistant/components/plex/translations/sl.json @@ -11,10 +11,22 @@ }, "error": { "faulty_credentials": "Avtorizacija ni uspela", + "host_or_token": "Zagotoviti morate vsaj enega gostitelja ali \u017eeton", "no_servers": "Ni stre\u017enikov povezanih z ra\u010dunom", - "not_found": "Plex stre\u017enika ni mogo\u010de najti" + "not_found": "Plex stre\u017enika ni mogo\u010de najti", + "ssl_error": "Te\u017eava s SSL certifikatom" }, "step": { + "manual_setup": { + "data": { + "host": "Gostitelj (izbirno, \u010de je vnesen \u017eeton)", + "port": "Vrata", + "ssl": "Uporaba SSL", + "token": "\u017deton (izbirno)", + "verify_ssl": "Preverite SSL potrdilo" + }, + "title": "Ro\u010dna konfiguracija Plex" + }, "select_server": { "data": { "server": "Stre\u017enik" @@ -25,6 +37,16 @@ "start_website_auth": { "description": "Nadaljujte z avtorizacijo na plex.tv.", "title": "Pove\u017eite stre\u017enik Plex" + }, + "user": { + "description": "Nadaljujte do [plex.tv] (https://plex.tv), da pove\u017eete stre\u017enik Plex.", + "title": "Plex medijski stre\u017enik" + }, + "user_advanced": { + "data": { + "setup_method": "Na\u010din nastavitve" + }, + "title": "Plex medijski stre\u017enik" } } }, diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index ba35af1b326..61490e36c82 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -25,6 +25,15 @@ "start_website_auth": { "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv.", "title": "Anslut Plex-servern" + }, + "user": { + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "Inst\u00e4llningsmetod" + }, + "title": "Plex Media Server" } } }, @@ -32,6 +41,7 @@ "step": { "plex_mp_settings": { "data": { + "ignore_plex_web_clients": "Ignorera Plex Web-klienter", "use_episode_art": "Anv\u00e4nd avsnittsbild" }, "description": "Alternativ f\u00f6r Plex-mediaspelare" diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json index da8f675a657..4efd0fb99b6 100644 --- a/homeassistant/components/plex/translations/zh-Hant.json +++ b/homeassistant/components/plex/translations/zh-Hant.json @@ -10,11 +10,24 @@ "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, "error": { - "faulty_credentials": "\u9a57\u8b49\u5931\u6557", - "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668", - "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" + "faulty_credentials": "\u9a57\u8b49\u5931\u6557\u3001\u78ba\u8a8d\u5bc6\u9470", + "host_or_token": "\u5fc5\u9808\u81f3\u5c11\u63d0\u4f9b\u4e3b\u6a5f\u7aef\u6216\u5bc6\u9470", + "no_servers": "Plex \u5e33\u865f\u672a\u7d81\u5b9a\u4efb\u4f55\u4f3a\u670d\u5668", + "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668", + "ssl_error": "SSL \u8a8d\u8b49\u554f\u984c" }, + "flow_title": "{name} ({host})", "step": { + "manual_setup": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL", + "token": "\u5bc6\u9470\uff08\u9078\u9805\uff09", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "title": "Plex \u624b\u52d5\u8a2d\u5b9a" + }, "select_server": { "data": { "server": "\u4f3a\u670d\u5668" @@ -25,6 +38,16 @@ "start_website_auth": { "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + }, + "user": { + "description": "\u7e7c\u7e8c\u81f3 [plex.tv](https://plex.tv) \u4ee5\u9023\u7d50\u4e00\u7d44 Plex \u4f3a\u670d\u5668\u3002", + "title": "Plex \u5a92\u9ad4\u4f3a\u670d\u5668" + }, + "user_advanced": { + "data": { + "setup_method": "\u8a2d\u5b9a\u6a21\u5f0f" + }, + "title": "Plex \u5a92\u9ad4\u4f3a\u670d\u5668" } } }, @@ -33,6 +56,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5206\u4eab\u4f7f\u7528\u8005", + "ignore_plex_web_clients": "\u5ffd\u7565 Plex \u7db2\u9801\u5ba2\u6236\u7aef", "monitored_users": "\u5df2\u76e3\u63a7\u4f7f\u7528\u8005", "use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167" }, diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index aef1dd78197..8e2e525217a 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -5,7 +5,7 @@ import logging import haanna import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -88,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class ThermostatDevice(ClimateDevice): +class ThermostatDevice(ClimateEntity): """Representation of the Plugwise thermostat.""" def __init__(self, api, name, min_temp, max_temp): diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 9f14f6c6e61..55e43c2a29f 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -3,5 +3,5 @@ "name": "Plugwise Anna", "documentation": "https://www.home-assistant.io/integrations/plugwise", "codeowners": ["@laetificat", "@CoMPaTech", "@bouwew"], - "requirements": ["haanna==0.14.3"] + "requirements": ["haanna==0.15.0"] } diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 1ce76d9dc5f..737c6f2bfad 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -4,7 +4,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) import homeassistant.util.color as color_util @@ -32,7 +32,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class PlumLight(Light): +class PlumLight(LightEntity): """Representation of a Plum Lightpad dimmer.""" def __init__(self, load): @@ -88,7 +88,7 @@ class PlumLight(Light): await self._load.turn_off() -class GlowRing(Light): +class GlowRing(LightEntity): """Representation of a Plum Lightpad dimmer glow ring.""" def __init__(self, lightpad): diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index d84c408e43a..25e135f59cb 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -1,7 +1,7 @@ """Support for Minut Point.""" import logging -from homeassistant.components.alarm_control_panel import DOMAIN, AlarmControlPanel +from homeassistant.components.alarm_control_panel import DOMAIN, AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MinutPointAlarmControl(AlarmControlPanel): +class MinutPointAlarmControl(AlarmControlPanelEntity): """The platform class required by Home Assistant.""" def __init__(self, point_client, home_id): diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index b417c128913..5a780c2e57a 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Minut Point binary sensors.""" import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): +class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): """The platform class required by Home Assistant.""" def __init__(self, point_client, device_id, device_class): diff --git a/homeassistant/components/point/strings.json b/homeassistant/components/point/strings.json index f3b5fd5c4c4..194121e8e25 100644 --- a/homeassistant/components/point/strings.json +++ b/homeassistant/components/point/strings.json @@ -2,27 +2,27 @@ "config": { "step": { "user": { - "title": "Authentication Provider", - "description": "Pick via which authentication provider you want to authenticate with Point.", + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]", + "description": "[%key:common::config_flow::description::confirm_setup%]", "data": { "flow_impl": "Provider" } }, "auth": { "title": "Authenticate Point", - "description": "Please follow the link below and Accept access to your Minut account, then come back and press Submit below.\n\n[Link]({authorization_url})" + "description": "Please follow the link below and **Accept** access to your Minut account, then come back and press **Submit** below.\n\n[Link]({authorization_url})" } }, "create_entry": { - "default": "Successfully authenticated with Minut for your Point device(s)" + "default": "[%key:common::config_flow::create_entry::authenticated%]" }, "error": { - "no_token": "Not authenticated with Minut", + "no_token": "[%key:common::config_flow::error::invalid_access_token%]", "follow_link": "Please follow the link and authenticate before pressing Submit" }, "abort": { - "already_setup": "You can only configure a Point account.", + "already_setup": "[%key:common::config_flow::abort::single_instance_allowed%]", "external_setup": "Point successfully configured from another flow.", - "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/).", - "authorize_url_timeout": "Timeout generating authorize url.", + "no_flows": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "authorize_url_fail": "Unknown error generating an authorize url." } } diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index 6ff775a8a0e..84674cafb89 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s pots configurar un compte de Point.", + "already_setup": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.", - "no_flows": "Necessites configurar Point abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/point/)." + "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, "create_entry": { - "default": "Autenticaci\u00f3 exitosa amb Minut per als teus dispositiu/s Point." + "default": "Autenticaci\u00f3 exitosa" }, "error": { "follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia", - "no_token": "No s'ha autenticat amb Minut" + "no_token": "Token d'acc\u00e9s no v\u00e0lid" }, "step": { "auth": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "Prove\u00efdor" }, - "description": "Tria quin prove\u00efdor d'autenticaci\u00f3 vols utilitzar per autenticar-te amb Point.", - "title": "Prove\u00efdor d'autenticaci\u00f3" + "description": "Vols comen\u00e7ar la configuraci\u00f3?", + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" } } } diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index e55bb54da46..e7f34fa1b76 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_setup": "You can only configure a Point account.", + "already_setup": "Already configured. Only a single configuration possible.", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "external_setup": "Point successfully configured from another flow.", - "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/)." + "no_flows": "The component is not configured. Please follow the documentation." }, "create_entry": { - "default": "Successfully authenticated with Minut for your Point device(s)" + "default": "Successfully authenticated" }, "error": { "follow_link": "Please follow the link and authenticate before pressing Submit", - "no_token": "Not authenticated with Minut" + "no_token": "Invalid access token" }, "step": { "auth": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "Provider" }, - "description": "Pick via which authentication provider you want to authenticate with Point.", - "title": "Authentication Provider" + "description": "Do you want to start set up?", + "title": "Pick Authentication Method" } } } diff --git a/homeassistant/components/point/translations/es-419.json b/homeassistant/components/point/translations/es-419.json index 2b6e51ba4a9..2b177e26825 100644 --- a/homeassistant/components/point/translations/es-419.json +++ b/homeassistant/components/point/translations/es-419.json @@ -3,7 +3,12 @@ "abort": { "already_setup": "Solo puede configurar una cuenta Point.", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", - "external_setup": "Punto configurado con \u00e9xito desde otro flujo." + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "external_setup": "Punto configurado con \u00e9xito desde otro flujo.", + "no_flows": "Debe configurar Point antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Minut para sus dispositivos Point" }, "error": { "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", @@ -11,7 +16,8 @@ }, "step": { "auth": { - "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceso a su cuenta de Minut, luego vuelva y presione Enviar continuaci\u00f3n. \n\n [Enlace] ( {authorization_url} )" + "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceso a su cuenta de Minut, luego vuelva y presione Enviar continuaci\u00f3n. \n\n [Enlace] ( {authorization_url} )", + "title": "Autenticaci\u00f3n Point" }, "user": { "data": { diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 0589f7c8468..04e0498fcdf 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "S\u00f3lo se puede configurar una cuenta de Point.", + "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", diff --git a/homeassistant/components/point/translations/fi.json b/homeassistant/components/point/translations/fi.json new file mode 100644 index 00000000000..8b7c30df298 --- /dev/null +++ b/homeassistant/components/point/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "flow_impl": "Tarjoaja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 62225188d8a..90f9c1b73cd 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_setup": "\u00c8 possibile configurare un solo account Point.", + "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", - "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "external_setup": "Point configurato correttamente da un altro flusso.", - "no_flows": "Devi configurare Point prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/point/)." + "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." }, "create_entry": { - "default": "Autenticato con successo con Minut per i tuoi dispositivi Point" + "default": "Autenticazione riuscita" }, "error": { "follow_link": "Segui il link e autenticati prima di premere Invio", - "no_token": "Non autenticato con Minut" + "no_token": "Token di accesso non valido" }, "step": { "auth": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "Provider" }, - "description": "Scegli tramite quale provider di autenticazione vuoi autenticarti con Point.", - "title": "Provider di autenticazione" + "description": "Vuoi iniziare la configurazione?", + "title": "Scegli il metodo di autenticazione" } } } diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index a121d9bb460..8a4eb14f287 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -1,30 +1,30 @@ { "config": { "abort": { - "already_setup": "\ud558\ub098\uc758 Point \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "external_setup": "Point \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", - "no_flows": "Point \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Point \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/point/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { - "default": "Point \uae30\uae30\ub85c Minut \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", - "no_token": "Minut \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" + "no_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "auth": { "description": "\uc544\ub798 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud558\uc5ec Minut \uacc4\uc815\uc5d0 \ub300\ud574 \ub3d9\uc758 \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n\n[\ub9c1\ud06c] ({authorization_url})", - "title": "Point \uc778\uc99d" + "title": "Point \uc778\uc99d\ud558\uae30" }, "user": { "data": { "flow_impl": "\uacf5\uae09\uc790" }, - "description": "Point \ub97c \uc778\uc99d\ud558\uae30 \uc704\ud55c \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "\uc778\uc99d \uacf5\uae09\uc790" + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index e275d0f231e..30ac5f8e356 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -2,22 +2,22 @@ "config": { "abort": { "already_setup": "Du kan kun konfigurere \u00e9n Point-konto.", - "authorize_url_fail": "Ukjent feil ved generering en autoriseringsadresse.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", "external_setup": "Punktet er konfigurert fra en annen flyt.", - "no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)." + "no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kangodkjenne den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)." }, "create_entry": { - "default": "Vellykket autentisering med Minut for din(e) Point enhet(er)" + "default": "Vellykket godkjenning med Minut for din(e) Point enhet(er)" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og autentiser f\u00f8r du trykker p\u00e5 Send", - "no_token": "Ikke autentisert med Minut" + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", + "no_token": "Ikke godkjent med Minut" }, "step": { "auth": { "description": "Vennligst f\u00f8lg lenken nedenfor og Godta tilgang til Minut-kontoen din, kom tilbake og trykk Send inn nedenfor. \n\n [Link]({authorization_url})", - "title": "Godkjenne Point" + "title": "Godkjenn Point" }, "user": { "data": { diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json index d13d3201546..22fc5da2278 100644 --- a/homeassistant/components/point/translations/pl.json +++ b/homeassistant/components/point/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko konto Point.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", "no_flows": "Musisz skonfigurowa\u0107 Point, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/point/)." }, diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json index 972a232036e..8c481dc0305 100644 --- a/homeassistant/components/point/translations/ru.json +++ b/homeassistant/components/point/translations/ru.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." + "no_flows": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", - "no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430." + "no_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." }, "step": { "auth": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d \u0432\u0445\u043e\u0434.", - "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } } diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index 0337dad22ef..618480cb771 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Point \u5e33\u865f\u3002", + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", - "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/point/)\u3002" + "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", - "no_token": "Minut \u672a\u6388\u6b0a" + "no_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548" }, "step": { "auth": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "\u63d0\u4f9b\u8005" }, - "description": "\u65bc\u8a8d\u8b49\u63d0\u4f9b\u8005\u4e2d\u6311\u9078\u6240\u8981\u9032\u884c Point \u8a8d\u8b49\u63d0\u4f9b\u8005\u3002", - "title": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } } diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index a76393e350a..fa9c81533e7 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging import requests -from tesla_powerwall import APIError, Powerwall, PowerwallUnreachableError +from tesla_powerwall import APIChangedError, Powerwall, PowerwallUnreachableError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -13,10 +13,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( DOMAIN, + POWERWALL_API_CHANGED, POWERWALL_API_CHARGE, POWERWALL_API_DEVICE_TYPE, POWERWALL_API_GRID_STATUS, @@ -64,7 +65,7 @@ async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): @callback def _async_migrator(entity_entry: entity_registry.RegistryEntry): parts = entity_entry.unique_id.split("_") - # Check if the unique_id starts with the serial_numbers of the powerwakks + # Check if the unique_id starts with the serial_numbers of the powerwalls if parts[0 : len(serial_numbers)] != serial_numbers: # The old unique_id ended with the nomianal_system_engery_kWh so we can use that # to find the old base unique_id and extract the device_suffix. @@ -87,6 +88,17 @@ async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): await entity_registry.async_migrate_entries(hass, entry_id, _async_migrator) +async def _async_handle_api_changed_error(hass: HomeAssistant, error: APIChangedError): + # The error might include some important information about what exactly changed. + _LOGGER.error(str(error)) + hass.components.persistent_notification.async_create( + "It seems like your powerwall uses an unsupported version. " + "Please update the software of your powerwall or if it is" + "already the newest consider reporting this issue.\nSee logs for more information", + title="Unknown powerwall software version", + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Tesla Powerwall from a config entry.""" @@ -97,16 +109,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session) try: await hass.async_add_executor_job(power_wall.detect_and_pin_version) + await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) powerwall_data = await hass.async_add_executor_job(call_base_info, power_wall) - except (PowerwallUnreachableError, APIError, ConnectionError): + except PowerwallUnreachableError: http_session.close() raise ConfigEntryNotReady + except APIChangedError as err: + http_session.close() + await _async_handle_api_changed_error(hass, err) + return False await _migrate_old_unique_ids(hass, entry_id, powerwall_data) async def async_update_data(): """Fetch data from API endpoint.""" - return await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) + # Check if we had an error before + _LOGGER.debug("Checking if update failed") + if not hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: + _LOGGER.debug("Updating data") + try: + return await hass.async_add_executor_job( + _fetch_powerwall_data, power_wall + ) + except PowerwallUnreachableError: + raise UpdateFailed("Unable to fetch data from powerwall") + except APIChangedError as err: + await _async_handle_api_changed_error(hass, err) + hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True + # Returns the cached data. This data can also be None + return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data + else: + return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data coordinator = DataUpdateCoordinator( hass, @@ -122,6 +155,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): POWERWALL_OBJECT: power_wall, POWERWALL_COORDINATOR: coordinator, POWERWALL_HTTP_SESSION: http_session, + POWERWALL_API_CHANGED: False, } ) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 877efdd68fa..160a62a2029 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -6,7 +6,7 @@ from tesla_powerwall import GridStatus from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_CONNECTIVITY, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import DEVICE_CLASS_POWER @@ -56,7 +56,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class PowerWallRunningSensor(PowerWallEntity, BinarySensorDevice): +class PowerWallRunningSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall running sensor.""" @property @@ -89,7 +89,7 @@ class PowerWallRunningSensor(PowerWallEntity, BinarySensorDevice): } -class PowerWallConnectedSensor(PowerWallEntity, BinarySensorDevice): +class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall connected sensor.""" @property @@ -113,7 +113,7 @@ class PowerWallConnectedSensor(PowerWallEntity, BinarySensorDevice): return self._coordinator.data[POWERWALL_API_SITEMASTER].connected_to_tesla -class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): +class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall grid status sensor.""" @property @@ -137,7 +137,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): return self._coordinator.data[POWERWALL_API_GRID_STATUS] == GridStatus.CONNECTED -class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorDevice): +class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall charging status sensor.""" @property diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index ca0e2143454..8c313b79024 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Tesla Powerwall integration.""" import logging -from tesla_powerwall import APIError, Powerwall, PowerwallUnreachableError +from tesla_powerwall import APIChangedError, Powerwall, PowerwallUnreachableError import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -25,8 +25,12 @@ async def validate_input(hass: core.HomeAssistant, data): try: await hass.async_add_executor_job(power_wall.detect_and_pin_version) site_info = await hass.async_add_executor_job(power_wall.get_site_info) - except (PowerwallUnreachableError, APIError, ConnectionError): + except PowerwallUnreachableError: raise CannotConnect + except APIChangedError as err: + # Only log the exception without the traceback + _LOGGER.error(str(err)) + raise WrongVersion # Return info that you want to store in the config entry. return {"title": site_info.site_name} @@ -46,6 +50,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" + except WrongVersion: + errors["base"] = "wrong_version" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -69,3 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" + + +class WrongVersion(exceptions.HomeAssistantError): + """Error to indicate the powerwall uses a software version we cannot interact with.""" diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index e2daf1e3760..5f0e9ae3b35 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -4,6 +4,7 @@ DOMAIN = "powerwall" POWERWALL_OBJECT = "powerwall" POWERWALL_COORDINATOR = "coordinator" +POWERWALL_API_CHANGED = "api_changed" UPDATE_INTERVAL = 30 @@ -16,14 +17,6 @@ ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" ATTR_NOMINAL_SYSTEM_POWER = "nominal_system_power_kW" ATTR_IS_ACTIVE = "is_active" -SITE_INFO_UTILITY = "utility" -SITE_INFO_GRID_CODE = "grid_code" -SITE_INFO_NOMINAL_SYSTEM_POWER_KW = "nominal_system_power_kW" -SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH = "nominal_system_energy_kWh" -SITE_INFO_REGION = "region" - -DEVICE_TYPE_DEVICE_TYPE = "device_type" - STATUS_VERSION = "version" POWERWALL_SITE_NAME = "site_name" @@ -39,10 +32,6 @@ POWERWALL_API_SERIAL_NUMBERS = "serial_numbers" POWERWALL_HTTP_SESSION = "http_session" -POWERWALL_GRID_ONLINE = "SystemGridConnected" -POWERWALL_CONNECTED_KEY = "connected_to_tesla" -POWERWALL_RUNNING_KEY = "running" - POWERWALL_BATTERY_METER = "battery" MODEL = "PowerWall 2" diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index bb5ed671435..ce7e6a1965f 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -8,6 +8,7 @@ }, "error": { "cannot_connect": "Failed to connect, please try again", + "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved.", "unknown": "Unexpected error" }, "abort": { "already_configured": "The powerwall is already configured" } diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 2416a2bf7f2..b0764b78234 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", "unknown": "Error inesperat", - "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d\u2019aquest problema perqu\u00e8 sigui solucionat." + "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 378abf486ad..8b45c665d85 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/es-419.json b/homeassistant/components/powerwall/translations/es-419.json new file mode 100644 index 00000000000..fe8f6d061a9 --- /dev/null +++ b/homeassistant/components/powerwall/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El powerwall ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado", + "wrong_version": "Su powerwall utiliza una versi\u00f3n de software que no es compatible. Considere actualizar o informar este problema para que pueda resolverse." + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + }, + "title": "Conectar el powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 2f8ffcf787f..76835e81480 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", "unknown": "Error inesperado", - "wrong_version": "tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." + "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index d7e0c759bbf..3ddc6634557 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json new file mode 100644 index 00000000000..7cc0ceafac1 --- /dev/null +++ b/homeassistant/components/powerwall/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP-c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 09d103c6f60..09a3d570801 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Impossibile connettersi, si prega di riprovare", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/ko.json b/homeassistant/components/powerwall/translations/ko.json index 7922a11845c..9ba7004899f 100644 --- a/homeassistant/components/powerwall/translations/ko.json +++ b/homeassistant/components/powerwall/translations/ko.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "wrong_version": "Powerwall \uc774 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ubc84\uc804\uc758 \uc18c\ud504\ud2b8\uc6e8\uc5b4\ub97c \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4. \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\ub824\uba74 \uc5c5\uadf8\ub808\uc774\ub4dc\ud558\uac70\ub098 \uc774 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json new file mode 100644 index 00000000000..f77cc864813 --- /dev/null +++ b/homeassistant/components/powerwall/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "De powerwall is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout", + "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres" + }, + "title": "Maak verbinding met de powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index cfb277179ed..3ccdfce648a 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -5,13 +5,13 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d.", + "unknown": "[%key_id:common::config_flow::error::unknown%]", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, "step": { "user": { "data": { - "ip_address": "Adres IP" + "ip_address": "[%key_id:common::config_flow::data::ip%]" }, "title": "Po\u0142\u0105czenie z Powerwall" } diff --git a/homeassistant/components/powerwall/translations/sl.json b/homeassistant/components/powerwall/translations/sl.json index 122d8bcfd02..f0e465b143a 100644 --- a/homeassistant/components/powerwall/translations/sl.json +++ b/homeassistant/components/powerwall/translations/sl.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Povezava ni uspela, poskusite znova", - "unknown": "Nepri\u010dakovana napaka" + "unknown": "Nepri\u010dakovana napaka", + "wrong_version": "Va\u0161 powerwall uporablja razli\u010dico programske opreme, ki ni podprta. Razmislite o nadgradnji ali poro\u010danju o tej te\u017eavi, da jo boste lahko re\u0161ili." }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json new file mode 100644 index 00000000000..0c6f94cd697 --- /dev/null +++ b/homeassistant/components/powerwall/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel", + "wrong_version": "Powerwall anv\u00e4nder en programvaruversion som inte st\u00f6ds. T\u00e4nk p\u00e5 att uppgradera eller rapportera det h\u00e4r problemet s\u00e5 att det kan l\u00f6sas." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 44e31e24fb0..5dff4725ea0 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -2,8 +2,12 @@ import proliphix import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -37,17 +41,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) pdp = proliphix.PDP(host, username, password) + pdp.update() add_entities([ProliphixThermostat(pdp)], True) -class ProliphixThermostat(ClimateDevice): +class ProliphixThermostat(ClimateEntity): """Representation a Proliphix thermostat.""" def __init__(self, pdp): """Initialize the thermostat.""" self._pdp = pdp - self._name = self._pdp.name + self._name = None @property def supported_features(self): @@ -62,6 +67,7 @@ class ProliphixThermostat(ClimateDevice): def update(self): """Update the data from the thermostat.""" self._pdp.update() + self._name = self._pdp.name @property def name(self): @@ -97,16 +103,26 @@ class ProliphixThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._pdp.setback + @property + def hvac_action(self): + """Return the current state of the thermostat.""" + state = self._pdp.hvac_state + if state == 1: + return CURRENT_HVAC_OFF + if state in (3, 4, 5): + return CURRENT_HVAC_HEAT + if state in (6, 7): + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE + @property def hvac_mode(self): """Return the current state of the thermostat.""" - state = self._pdp.hvac_mode - if state in (1, 2): - return HVAC_MODE_OFF - if state == 3: + if self._pdp.is_heating: return HVAC_MODE_HEAT - if state == 6: + if self._pdp.is_cooling: return HVAC_MODE_COOL + return HVAC_MODE_OFF @property def hvac_modes(self): diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 15b1f1483e1..698a2c35ae1 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,7 +1,7 @@ """Binary sensor to read Proxmox VE data.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class ProxmoxBinarySensor(BinarySensorDevice): +class ProxmoxBinarySensor(BinarySensorEntity): """A binary sensor for reading Proxmox VE data.""" def __init__(self, proxmox_client, item_node, item_type, item_id): diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 89d25ca23b1..ffa659f979e 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==7.1.1"], + "requirements": ["pillow==7.1.2"], "codeowners": [] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 3acaf75d699..39b60be0493 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -5,7 +5,7 @@ import logging from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete import pyps4_2ndscreen.ps4 as pyps4 -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, @@ -69,7 +69,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(device_list, update_before_add=True) -class PS4Device(MediaPlayerDevice): +class PS4Device(MediaPlayerEntity): """Representation of a PS4.""" def __init__(self, config, name, host, region, ps4, creds): diff --git a/homeassistant/components/ps4/translations/es-419.json b/homeassistant/components/ps4/translations/es-419.json index eabbc340cc8..c718c4469aa 100644 --- a/homeassistant/components/ps4/translations/es-419.json +++ b/homeassistant/components/ps4/translations/es-419.json @@ -8,7 +8,9 @@ "port_997_bind_error": "No se pudo enlazar al puerto 997." }, "error": { + "credential_timeout": "Servicio de credenciales agotado. Presione enviar para reiniciar.", "login_failed": "No se ha podido emparejar con PlayStation 4. Verifique que el PIN sea correcto.", + "no_ipaddress": "Ingrese la direcci\u00f3n IP de la PlayStation 4 que desea configurar.", "not_ready": "PlayStation 4 no est\u00e1 encendida o conectada a la red." }, "step": { @@ -28,8 +30,10 @@ }, "mode": { "data": { + "ip_address": "Direcci\u00f3n IP (dejar en blanco si se utiliza el descubrimiento autom\u00e1tico).", "mode": "Modo de configuraci\u00f3n" }, + "description": "Seleccione el modo para la configuraci\u00f3n. El campo Direcci\u00f3n IP puede dejarse en blanco si selecciona Descubrimiento autom\u00e1tico, ya que los dispositivos se descubrir\u00e1n autom\u00e1ticamente.", "title": "Playstation 4" } } diff --git a/homeassistant/components/ps4/translations/fi.json b/homeassistant/components/ps4/translations/fi.json new file mode 100644 index 00000000000..f61b9f0c162 --- /dev/null +++ b/homeassistant/components/ps4/translations/fi.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "creds": { + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP-osoite", + "name": "Nimi", + "region": "Alue" + }, + "title": "Playstation 4" + }, + "mode": { + "data": { + "ip_address": "IP-osoite (j\u00e4t\u00e4 tyhj\u00e4ksi, jos k\u00e4yt\u00e4t automaattista etsint\u00e4\u00e4).", + "mode": "Konfigurointitila" + }, + "title": "Playstation 4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index cd9b80175ad..814f09095a2 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -5,12 +5,12 @@ "devices_configured": "Alle enheter som ble funnet er allerede konfigurert.", "no_devices_found": "Ingen PlayStation 4 enheter funnet p\u00e5 nettverket.", "port_987_bind_error": "Kunne ikke binde til port 987. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for mer info.", - "port_997_bind_error": "Kunne ikke binde til port 997. Se [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for videre informasjon." + "port_997_bind_error": "Kunne ikke binde til port 997. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for videre informasjon." }, "error": { "credential_timeout": "Legitimasjonstjenesten ble tidsavbrutt. Trykk send for \u00e5 starte p\u00e5 nytt.", "login_failed": "Klarte ikke \u00e5 koble til PlayStation 4. Bekreft at PIN koden er riktig.", - "no_ipaddress": "Angi IP adressen til din PlayStation 4 som du \u00f8nsker konfigurere.", + "no_ipaddress": "Fyll inn IP adressen til din PlayStation 4 som du \u00f8nsker konfigurere.", "not_ready": "PlayStation 4 er ikke p\u00e5sl\u00e5tt eller koblet til nettverk." }, "step": { @@ -20,20 +20,20 @@ }, "link": { "data": { - "code": "PIN", + "code": "", "ip_address": "IP adresse", "name": "Navn", - "region": "Region" + "region": "" }, - "description": "Skriv inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Tast inn PIN-koden som vises. Se [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", + "description": "Fyll inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsoll. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Fyll inn PIN-koden som vises. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for mer informasjon.", "title": "" }, "mode": { "data": { - "ip_address": "IP- adresse (Ikke fyll ut hvis du bruker Auto Discovery).", + "ip_address": "IP-adresse (La st\u00e5 tom hvis du bruker Automatisk Oppdagelse).", "mode": "Konfigureringsmodus" }, - "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Auto Discovery, da enheter vil bli oppdaget automatisk.", + "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Automatisk Oppdagelse, da enheter vil bli oppdaget automatisk.", "title": "" } } diff --git a/homeassistant/components/ps4/translations/pl.json b/homeassistant/components/ps4/translations/pl.json index f13c5b00bb5..a701feb6f32 100644 --- a/homeassistant/components/ps4/translations/pl.json +++ b/homeassistant/components/ps4/translations/pl.json @@ -21,7 +21,7 @@ "link": { "data": { "code": "PIN", - "ip_address": "Adres IP", + "ip_address": "[%key_id:common::config_flow::data::ip%]", "name": "Nazwa", "region": "Region" }, @@ -30,7 +30,7 @@ }, "mode": { "data": { - "ip_address": "Adres IP (pozostaw puste, je\u015bli u\u017cywasz funkcji Auto Discovery).", + "ip_address": "[%key_id:common::config_flow::data::ip%] (pozostaw puste, je\u015bli u\u017cywasz wykrywania).", "mode": "Tryb konfiguracji" }, "description": "Wybierz tryb konfiguracji. Pole adresu IP mo\u017cna pozostawi\u0107 puste, je\u015bli wybierzesz opcj\u0119 Auto Discovery, poniewa\u017c urz\u0105dzenia zostan\u0105 automatycznie wykryte.", diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index 8775f5f0947..bc38d8c2594 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -2,5 +2,6 @@ "domain": "pulseaudio_loopback", "name": "PulseAudio Loopback", "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", + "requirements": ["pulsectl==20.2.4"], "codeowners": [] } diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index ec1adc7641b..9c27ab4e027 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -1,52 +1,32 @@ """Switch logic for loading/unloading pulseaudio loopback modules.""" -from datetime import timedelta import logging -import re -import socket +from pulsectl import Pulse, PulseError import voluptuous as vol -from homeassistant import util -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) -_PULSEAUDIO_SERVERS = {} +DOMAIN = "pulseaudio_loopback" + +_LOGGER = logging.getLogger(__name__) -CONF_BUFFER_SIZE = "buffer_size" CONF_SINK_NAME = "sink_name" CONF_SOURCE_NAME = "source_name" -CONF_TCP_TIMEOUT = "tcp_timeout" -DEFAULT_BUFFER_SIZE = 1024 -DEFAULT_HOST = "localhost" DEFAULT_NAME = "paloopback" -DEFAULT_PORT = 4712 -DEFAULT_TCP_TIMEOUT = 3 +DEFAULT_PORT = 4713 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." -LOAD_CMD = "load-module module-loopback sink={0} source={1}" - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MOD_REGEX = ( - r"index: ([0-9]+)\s+name: " - r"\s+argument: (?=<.*sink={0}.*>)(?=<.*source={1}.*>)" -) - -UNLOAD_CMD = "unload-module {0}" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_SINK_NAME): cv.string, vol.Required(CONF_SOURCE_NAME): cv.string, - vol.Optional(CONF_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE): cv.positive_int, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TCP_TIMEOUT, default=DEFAULT_TCP_TIMEOUT): cv.positive_int, } ) @@ -58,97 +38,62 @@ def setup_platform(hass, config, add_entities, discovery_info=None): source_name = config.get(CONF_SOURCE_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - buffer_size = config.get(CONF_BUFFER_SIZE) - tcp_timeout = config.get(CONF_TCP_TIMEOUT) + + hass.data.setdefault(DOMAIN, {}) server_id = str.format("{0}:{1}", host, port) - if server_id in _PULSEAUDIO_SERVERS: - server = _PULSEAUDIO_SERVERS[server_id] + if host: + connect_to_server = server_id else: - server = PAServer(host, port, buffer_size, tcp_timeout) - _PULSEAUDIO_SERVERS[server_id] = server + connect_to_server = None - add_entities([PALoopbackSwitch(hass, name, server, sink_name, source_name)]) + if server_id in hass.data[DOMAIN]: + server = hass.data[DOMAIN][server_id] + else: + server = Pulse(server=connect_to_server, connect=False, threading_lock=True) + hass.data[DOMAIN][server_id] = server + + add_entities([PALoopbackSwitch(name, server, sink_name, source_name)], True) -class PAServer: - """Representation of a Pulseaudio server.""" - - _current_module_state = "" - - def __init__(self, host, port, buff_sz, tcp_timeout): - """Initialize PulseAudio server.""" - self._pa_host = host - self._pa_port = int(port) - self._buffer_size = int(buff_sz) - self._tcp_timeout = int(tcp_timeout) - - def _send_command(self, cmd, response_expected): - """Send a command to the pa server using a socket.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(self._tcp_timeout) - try: - sock.connect((self._pa_host, self._pa_port)) - _LOGGER.info("Calling pulseaudio: %s", cmd) - sock.send((cmd + "\n").encode("utf-8")) - if response_expected: - return_data = self._get_full_response(sock) - _LOGGER.debug("Data received from pulseaudio: %s", return_data) - else: - return_data = "" - finally: - sock.close() - return return_data - - def _get_full_response(self, sock): - """Get the full response back from pulseaudio.""" - result = "" - rcv_buffer = sock.recv(self._buffer_size) - result += rcv_buffer.decode("utf-8") - - while len(rcv_buffer) == self._buffer_size: - rcv_buffer = sock.recv(self._buffer_size) - result += rcv_buffer.decode("utf-8") - - return result - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update_module_state(self): - """Refresh state in case an alternate process modified this data.""" - self._current_module_state = self._send_command("list-modules", True) - - def turn_on(self, sink_name, source_name): - """Send a command to pulseaudio to turn on the loopback.""" - self._send_command(str.format(LOAD_CMD, sink_name, source_name), False) - - def turn_off(self, module_idx): - """Send a command to pulseaudio to turn off the loopback.""" - self._send_command(str.format(UNLOAD_CMD, module_idx), False) - - def get_module_idx(self, sink_name, source_name): - """For a sink/source, return its module id in our cache, if found.""" - result = re.search( - str.format(MOD_REGEX, re.escape(sink_name), re.escape(source_name)), - self._current_module_state, - ) - if result and result.group(1).isdigit(): - return int(result.group(1)) - return -1 - - -class PALoopbackSwitch(SwitchDevice): +class PALoopbackSwitch(SwitchEntity): """Representation the presence or absence of a PA loopback module.""" - def __init__(self, hass, name, pa_server, sink_name, source_name): + def __init__(self, name, pa_server, sink_name, source_name): """Initialize the Pulseaudio switch.""" - self._module_idx = -1 - self._hass = hass + self._module_idx = None self._name = name self._sink_name = sink_name self._source_name = source_name self._pa_svr = pa_server + def _get_module_idx(self): + try: + self._pa_svr.connect() + + for module in self._pa_svr.module_list(): + if not module.name == "module-loopback": + continue + + if f"sink={self._sink_name}" not in module.argument: + continue + + if f"source={self._source_name}" not in module.argument: + continue + + return module.index + + except PulseError: + return None + + return None + + @property + def available(self): + """Return true when connected to server.""" + return self._pa_svr.connected + @property def name(self): """Return the name of the switch.""" @@ -157,35 +102,25 @@ class PALoopbackSwitch(SwitchDevice): @property def is_on(self): """Return true if device is on.""" - return self._module_idx > 0 + return self._module_idx is not None def turn_on(self, **kwargs): """Turn the device on.""" if not self.is_on: - self._pa_svr.turn_on(self._sink_name, self._source_name) - self._pa_svr.update_module_state(no_throttle=True) - self._module_idx = self._pa_svr.get_module_idx( - self._sink_name, self._source_name + self._pa_svr.module_load( + "module-loopback", + args=f"sink={self._sink_name} source={self._source_name}", ) - self.schedule_update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) def turn_off(self, **kwargs): """Turn the device off.""" if self.is_on: - self._pa_svr.turn_off(self._module_idx) - self._pa_svr.update_module_state(no_throttle=True) - self._module_idx = self._pa_svr.get_module_idx( - self._sink_name, self._source_name - ) - self.schedule_update_ha_state() + self._pa_svr.module_unload(self._module_idx) else: _LOGGER.warning(IGNORED_SWITCH_WARN) def update(self): """Refresh state in case an alternate process modified this data.""" - self._pa_svr.update_module_state() - self._module_idx = self._pa_svr.get_module_idx( - self._sink_name, self._source_name - ) + self._module_idx = self._get_module_idx() diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json index a96fa3b584b..8eeab499620 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json @@ -9,7 +9,7 @@ "name": "Nom del sensor", "tariff": "Tarifa contractada (1, 2 o 3 per\u00edodes)" }, - "description": "Aquest sensor utilitza l'API oficial de la xarxa el\u00e8ctrica espanyola (REE) per obtenir els [preus per hora de l\u2019electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecciona la tarifa contractada, cadascuna t\u00e9 un nombre determinat de per\u00edodes: \n - 1 per\u00edode: normal (sense discriminaci\u00f3)\n - 2 per\u00edodes: discriminaci\u00f3 (tarifa nocturna) \n - 3 per\u00edodes: cotxe el\u00e8ctric (tarifa nocturna de 3 per\u00edodes)", + "description": "Aquest sensor utilitza l'API oficial de la xarxa el\u00e8ctrica espanyola (REE) per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecciona la tarifa contractada, cadascuna t\u00e9 un nombre determinat de per\u00edodes: \n - 1 per\u00edode: normal (sense discriminaci\u00f3)\n - 2 per\u00edodes: discriminaci\u00f3 (tarifa nocturna) \n - 3 per\u00edodes: cotxe el\u00e8ctric (tarifa nocturna de 3 per\u00edodes)", "title": "Selecci\u00f3 de tarifa" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json b/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json new file mode 100644 index 00000000000..ed6ab16c8b5 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3n ya est\u00e1 configurada con un sensor existente con esa tarifa" + }, + "step": { + "user": { + "data": { + "name": "Nombre del sensor", + "tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)" + }, + "description": "Este sensor utiliza la API oficial para obtener [precios por hora de la electricidad (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espa\u00f1a. \n Para obtener una explicaci\u00f3n m\u00e1s precisa, visite los [documentos de integraci\u00f3n] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSeleccione la tarifa contratada en funci\u00f3n de la cantidad de per\u00edodos de facturaci\u00f3n por d\u00eda: \n - 1 per\u00edodo: normal \n - 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna) \n - 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", + "title": "Selecci\u00f3n de tarifa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index 5c615c5f757..4048e357751 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -6,8 +6,10 @@ "step": { "user": { "data": { - "name": "Nom du capteur" - } + "name": "Nom du capteur", + "tariff": "Tarif souscrit (1, 2, ou 3 p\u00e9riodes)" + }, + "title": "S\u00e9lection tarifaire" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json new file mode 100644 index 00000000000..3abffdf5bc0 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met dat tarief" + }, + "step": { + "user": { + "data": { + "name": "Sensornaam", + "tariff": "Gecontracteerd tarief (1, 2 of 3 periodes)" + }, + "description": "Deze sensor gebruikt de offici\u00eble API om [uurtarief voor elektriciteit (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanje te krijgen. \n Bezoek voor een meer precieze uitleg de [integratiedocumenten] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Selecteer het gecontracteerde tarief op basis van het aantal factureringsperioden per dag: \n - 1 periode: normaal \n - 2 periodes: discriminatie (nachttarief) \n - 3 periodes: elektrische auto (nachttarief van 3 periodes)", + "title": "Tariefselectie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index abcf1193ce4..6539479d2cd 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -60,7 +60,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: pyloadapi = PyLoadAPI(api_url=url, username=username, password=password) - pyloadapi.update() except ( requests.exceptions.ConnectionError, requests.exceptions.HTTPError, @@ -144,17 +143,11 @@ class PyLoadAPI: self.login = requests.post(f"{api_url}login", data=self.payload, timeout=5) self.update() - def post(self, method, params=None): + def post(self): """Send a POST request and return the response as a dict.""" - payload = {"method": method} - - if params: - payload["params"] = params - try: response = requests.post( f"{self.api_url}statusServer", - json=payload, cookies=self.login.cookies, headers=self.headers, timeout=5, @@ -170,4 +163,4 @@ class PyLoadAPI: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update cached response.""" - self.status = self.post("speed") + self.status = self.post() diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 46f82e99a62..4bf982bbbce 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -113,6 +113,7 @@ class QBittorrentSensor(Entity): except RequestException: _LOGGER.error("Connection lost") self._available = False + return except self._exception: _LOGGER.error("Invalid authentication") return diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 3b9df1a4e64..eaa813cae95 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,6 +2,6 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==7.1.1", "pyzbar==0.1.7"], + "requirements": ["pillow==7.1.2", "pyzbar==0.1.7"], "codeowners": [] } diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index ffe87797358..eea02fb9f54 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -75,7 +75,7 @@ class QSEntity(Entity): return self._name @property - def poll(self): + def should_poll(self): """QS sensors gets packets in update_packet.""" return False @@ -103,7 +103,7 @@ class QSToggleEntity(QSEntity): Implemented: - QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1]) - - QSSwitch extends QSToggleEntity and SwitchDevice[3] (ToggleEntity[1]) + - QSSwitch extends QSToggleEntity and SwitchEntity[3] (ToggleEntity[1]) [1] /helpers/entity.py [2] /components/light/__init__.py @@ -165,9 +165,9 @@ async def async_setup(hass, config): comps = {"switch": [], "light": [], "sensor": [], "binary_sensor": []} - try: - sensor_ids = [] - for sens in sensors: + sensor_ids = [] + for sens in sensors: + try: _, _type = SENSORS[sens["type"]] sensor_ids.append(sens["id"]) if _type is bool: @@ -179,9 +179,12 @@ async def async_setup(hass, config): _LOGGER.warning( "%s should only be used for binary_sensors: %s", _key, sens ) - - except KeyError: - _LOGGER.warning("Sensor validation failed") + except KeyError: + _LOGGER.warning( + "Sensor validation failed for sensor id=%s type=%s", + sens["id"], + sens["type"], + ) for qsid, dev in qsusb.devices.items(): if qsid in switches: diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index b3635dcb1f4..7a416843861 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -3,7 +3,7 @@ import logging from pyqwikswitch.qwikswitch import SENSORS -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from . import DOMAIN as QWIKSWITCH, QSEntity @@ -22,7 +22,7 @@ async def async_setup_platform(hass, _, add_entities, discovery_info=None): add_entities(devs) -class QSBinarySensor(QSEntity, BinarySensorDevice): +class QSBinarySensor(QSEntity, BinarySensorEntity): """Sensor based on a Qwikswitch relay/dimmer module.""" _val = False diff --git a/homeassistant/components/qwikswitch/light.py b/homeassistant/components/qwikswitch/light.py index 1adcef56ffa..43c6add6048 100644 --- a/homeassistant/components/qwikswitch/light.py +++ b/homeassistant/components/qwikswitch/light.py @@ -1,5 +1,5 @@ """Support for Qwikswitch Relays and Dimmers.""" -from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity from . import DOMAIN as QWIKSWITCH, QSToggleEntity @@ -14,7 +14,7 @@ async def async_setup_platform(hass, _, add_entities, discovery_info=None): add_entities(devs) -class QSLight(QSToggleEntity, Light): +class QSLight(QSToggleEntity, LightEntity): """Light based on a Qwikswitch relay/dimmer module.""" @property diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 9609af42f65..53cf68ccdba 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -34,8 +34,10 @@ class QSSensor(QSEntity): sensor_type = sensor["type"] self._decode, self.unit = SENSORS[sensor_type] - if isinstance(self.unit, type): - self.unit = f"{sensor_type}:{self.channel}" + # this cannot happen because it only happens in bool and this should be redirected to binary_sensor + assert not isinstance( + self.unit, type + ), f"boolean sensor id={sensor['id']} name={sensor['name']}" @callback def update_packet(self, packet): diff --git a/homeassistant/components/qwikswitch/switch.py b/homeassistant/components/qwikswitch/switch.py index 2d970a59a2a..61ef13f9e7a 100644 --- a/homeassistant/components/qwikswitch/switch.py +++ b/homeassistant/components/qwikswitch/switch.py @@ -1,5 +1,5 @@ """Support for Qwikswitch relays.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DOMAIN as QWIKSWITCH, QSToggleEntity @@ -14,5 +14,5 @@ async def async_setup_platform(hass, _, add_entities, discovery_info=None): add_entities(devs) -class QSSwitch(QSToggleEntity, SwitchDevice): +class QSSwitch(QSToggleEntity, SwitchEntity): """Switch based on a Qwikswitch relay module.""" diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index c3161cae6ab..49f46578f76 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -15,7 +15,6 @@ from .const import ( KEY_STATUS, KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, - STATUS_OFFLINE, STATUS_ONLINE, ) from .entity import RachioDevice @@ -38,16 +37,13 @@ def _create_entities(hass, config_entry): return entities -class RachioControllerBinarySensor(RachioDevice, BinarySensorDevice): +class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity): """Represent a binary sensor that reflects a Rachio state.""" - def __init__(self, controller, poll=True): + def __init__(self, controller): """Set up a new Rachio controller binary sensor.""" super().__init__(controller) - if poll: - self._state = self._poll_update() - else: - self._state = None + self._state = None @property def is_on(self) -> bool: @@ -64,10 +60,6 @@ class RachioControllerBinarySensor(RachioDevice, BinarySensorDevice): # For this device self._async_handle_update(args, kwargs) - @abstractmethod - def _poll_update(self, data=None) -> bool: - """Request the state from the API.""" - @abstractmethod def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" @@ -86,11 +78,6 @@ class RachioControllerBinarySensor(RachioDevice, BinarySensorDevice): class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Represent a binary sensor that reflects if the controller is online.""" - def __init__(self, controller): - """Set up a new Rachio controller online binary sensor.""" - super().__init__(controller, poll=False) - self._state = self._poll_update(controller.init_data) - @property def name(self) -> str: """Return the name of this sensor including the controller name.""" @@ -111,18 +98,10 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Return the name of an icon for this sensor.""" return "mdi:wifi-strength-4" if self.is_on else "mdi:wifi-strength-off-outline" - def _poll_update(self, data=None) -> bool: - """Request the state from the API.""" - if data is None: - data = self._controller.rachio.device.get(self._controller.controller_id)[1] - - if data[KEY_STATUS] == STATUS_ONLINE: - return True - if data[KEY_STATUS] == STATUS_OFFLINE: - return False - _LOGGER.warning( - '"%s" reported in unknown state "%s"', self.name, data[KEY_STATUS] - ) + async def async_added_to_hass(self): + """Get initial state.""" + self._state = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE + await super().async_added_to_hass() @callback def _async_handle_update(self, *args, **kwargs) -> None: diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 2f5835ad614..df9ead463f7 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -89,6 +89,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # they already have one configured as they can always # add a new one via "+" return self.async_abort(reason="already_configured") + properties = { + key.lower(): value for (key, value) in homekit_info["properties"].items() + } + await self.async_set_unique_id(properties["id"]) return await self.async_step_user() async def async_step_import(self, user_input): diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 3508e24eb4b..016218906f4 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -23,6 +23,7 @@ KEY_NAME = "name" KEY_MODEL = "model" KEY_ON = "on" KEY_DURATION = "totalDuration" +KEY_RAIN_DELAY = "rainDelayExpirationDate" KEY_STATUS = "status" KEY_SUBTYPE = "subType" KEY_SUMMARY = "summary" @@ -52,10 +53,10 @@ RACHIO_API_EXCEPTIONS = ( ) STATUS_ONLINE = "ONLINE" -STATUS_OFFLINE = "OFFLINE" SIGNAL_RACHIO_UPDATE = f"{DOMAIN}_update" SIGNAL_RACHIO_CONTROLLER_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_controller" +SIGNAL_RACHIO_RAIN_DELAY_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_rain_delay" SIGNAL_RACHIO_ZONE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_zone" SIGNAL_RACHIO_SCHEDULE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_schedule" diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 1c73e74902c..faea255693e 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -4,7 +4,9 @@ "user": { "title": "Connect to your Rachio device", "description": "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.", - "data": { "api_key": "The API key for the Rachio account." } + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } } }, "error": { @@ -12,7 +14,9 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } }, "options": { "step": { @@ -23,4 +27,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 764f87e924c..95f01d31518 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -3,9 +3,10 @@ from abc import abstractmethod from datetime import timedelta import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.dt import as_timestamp, now from .const import ( ATTR_ZONE_SHADE, @@ -22,17 +23,21 @@ from .const import ( KEY_IMAGE_URL, KEY_NAME, KEY_ON, + KEY_RAIN_DELAY, KEY_SCHEDULE_ID, KEY_SUBTYPE, KEY_SUMMARY, KEY_ZONE_ID, KEY_ZONE_NUMBER, SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_RAIN_DELAY_UPDATE, SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, ) from .entity import RachioDevice from .webhooks import ( + SUBTYPE_RAIN_DELAY_OFF, + SUBTYPE_RAIN_DELAY_ON, SUBTYPE_SCHEDULE_COMPLETED, SUBTYPE_SCHEDULE_STARTED, SUBTYPE_SCHEDULE_STOPPED, @@ -67,6 +72,7 @@ def _create_entities(hass, config_entry): # in order to avoid every zone doing it for controller in person.controllers: entities.append(RachioStandbySwitch(controller)) + entities.append(RachioRainDelay(controller)) zones = controller.list_zones() schedules = controller.list_schedules() flex_schedules = controller.list_flex_schedules() @@ -79,17 +85,13 @@ def _create_entities(hass, config_entry): return entities -class RachioSwitch(RachioDevice, SwitchDevice): +class RachioSwitch(RachioDevice, SwitchEntity): """Represent a Rachio state that can be toggled.""" - def __init__(self, controller, poll=True): + def __init__(self, controller): """Initialize a new Rachio switch.""" super().__init__(controller) - - if poll: - self._state = self._poll_update() - else: - self._state = None + self._state = None @property def name(self) -> str: @@ -101,10 +103,6 @@ class RachioSwitch(RachioDevice, SwitchDevice): """Return whether the switch is currently on.""" return self._state - @abstractmethod - def _poll_update(self, data=None) -> bool: - """Poll the API.""" - @callback def _async_handle_any_update(self, *args, **kwargs) -> None: """Determine whether an update event applies to this device.""" @@ -123,11 +121,6 @@ class RachioSwitch(RachioDevice, SwitchDevice): class RachioStandbySwitch(RachioSwitch): """Representation of a standby status/button.""" - def __init__(self, controller): - """Instantiate a new Rachio standby mode switch.""" - super().__init__(controller, poll=True) - self._poll_update(controller.init_data) - @property def name(self) -> str: """Return the name of the standby switch.""" @@ -143,13 +136,6 @@ class RachioStandbySwitch(RachioSwitch): """Return an icon for the standby switch.""" return "mdi:power" - def _poll_update(self, data=None) -> bool: - """Request the state from the API.""" - if data is None: - data = self._controller.rachio.device.get(self._controller.controller_id)[1] - - return not data[KEY_ON] - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" @@ -170,6 +156,9 @@ class RachioStandbySwitch(RachioSwitch): async def async_added_to_hass(self): """Subscribe to updates.""" + if KEY_ON in self._controller.init_data: + self._state = not self._controller.init_data[KEY_ON] + self.async_on_remove( async_dispatcher_connect( self.hass, @@ -179,6 +168,60 @@ class RachioStandbySwitch(RachioSwitch): ) +class RachioRainDelay(RachioSwitch): + """Representation of a rain delay status/switch.""" + + @property + def name(self) -> str: + """Return the name of the switch.""" + return f"{self._controller.name} rain delay" + + @property + def unique_id(self) -> str: + """Return a unique id by combining controller id and purpose.""" + return f"{self._controller.controller_id}-delay" + + @property + def icon(self) -> str: + """Return an icon for rain delay.""" + return "mdi:camera-timer" + + @callback + def _async_handle_update(self, *args, **kwargs) -> None: + """Update the state using webhook data.""" + if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_ON: + self._state = True + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_OFF: + self._state = False + + self.async_write_ha_state() + + def turn_on(self, **kwargs) -> None: + """Activate a 24 hour rain delay on the controller.""" + self._controller.rachio.device.rainDelay(self._controller.controller_id, 86400) + _LOGGER.debug("Starting rain delay for 24 hours") + + def turn_off(self, **kwargs) -> None: + """Resume controller functionality.""" + self._controller.rachio.device.rainDelay(self._controller.controller_id, 0) + _LOGGER.debug("Canceling rain delay") + + async def async_added_to_hass(self): + """Subscribe to updates.""" + if KEY_RAIN_DELAY in self._controller.init_data: + self._state = self._controller.init_data[ + KEY_RAIN_DELAY + ] / 1000 > as_timestamp(now()) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_RACHIO_RAIN_DELAY_UPDATE, + self._async_handle_any_update, + ) + ) + + class RachioZone(RachioSwitch): """Representation of one zone of sprinklers connected to the Rachio Iro.""" @@ -194,8 +237,7 @@ class RachioZone(RachioSwitch): self._zone_type = data.get(KEY_CUSTOM_CROP, {}).get(KEY_NAME) self._summary = "" self._current_schedule = current_schedule - super().__init__(controller, poll=False) - self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) + super().__init__(controller) def __str__(self): """Display the zone as a string.""" @@ -264,11 +306,6 @@ class RachioZone(RachioSwitch): """Stop watering all zones.""" self._controller.stop_watering() - def _poll_update(self, data=None) -> bool: - """Poll the API to check whether the zone is running.""" - self._current_schedule = self._controller.current_schedule - return self.zone_id == self._current_schedule.get(KEY_ZONE_ID) - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook zone data.""" @@ -286,6 +323,8 @@ class RachioZone(RachioSwitch): async def async_added_to_hass(self): """Subscribe to updates.""" + self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) + self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._async_handle_update @@ -304,8 +343,7 @@ class RachioSchedule(RachioSwitch): self._schedule_enabled = data[KEY_ENABLED] self._summary = data[KEY_SUMMARY] self._current_schedule = current_schedule - super().__init__(controller, poll=False) - self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) + super().__init__(controller) @property def name(self) -> str: @@ -320,7 +358,7 @@ class RachioSchedule(RachioSwitch): @property def icon(self) -> str: """Return the icon to display.""" - return "mdi:water" + return "mdi:water" if self.schedule_is_enabled else "mdi:water-off" @property def device_state_attributes(self) -> dict: @@ -348,11 +386,6 @@ class RachioSchedule(RachioSwitch): """Stop watering all zones.""" self._controller.stop_watering() - def _poll_update(self, data=None) -> bool: - """Poll the API to check whether the schedule is running.""" - self._current_schedule = self._controller.current_schedule - return self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook schedule data.""" @@ -373,6 +406,8 @@ class RachioSchedule(RachioSwitch): async def async_added_to_hass(self): """Subscribe to updates.""" + self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) + self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update diff --git a/homeassistant/components/rachio/translations/ca.json b/homeassistant/components/rachio/translations/ca.json index 8d4f9aa7245..14b4d8effd8 100644 --- a/homeassistant/components/rachio/translations/ca.json +++ b/homeassistant/components/rachio/translations/ca.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Durant quant de temps (en minuts) mantenir engegada una estaci\u00f3 quan l\u2019interruptor s'activa." + "manual_run_mins": "Durant quant de temps (en minuts) mantenir engegada una estaci\u00f3 quan l'interruptor s'activa." } } } diff --git a/homeassistant/components/rachio/translations/en.json b/homeassistant/components/rachio/translations/en.json index 364ca15b03f..21f7524008a 100644 --- a/homeassistant/components/rachio/translations/en.json +++ b/homeassistant/components/rachio/translations/en.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "The API key for the Rachio account." + "api_key": "API Key" }, "description": "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.", "title": "Connect to your Rachio device" diff --git a/homeassistant/components/rachio/translations/es-419.json b/homeassistant/components/rachio/translations/es-419.json new file mode 100644 index 00000000000..21186710e3d --- /dev/null +++ b/homeassistant/components/rachio/translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "La clave API para la cuenta Rachio." + }, + "description": "Necesitar\u00e1 la clave API de https://app.rach.io/. Seleccione 'Configuraci\u00f3n de la cuenta y luego haga clic en' OBTENER CLAVE API '.", + "title": "Con\u00e9ctese a su dispositivo Rachio" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Por cu\u00e1nto tiempo, en minutos, encender una estaci\u00f3n cuando el interruptor est\u00e1 habilitado." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/ko.json b/homeassistant/components/rachio/translations/ko.json index 1c3c8120d3b..2f5724c7af1 100644 --- a/homeassistant/components/rachio/translations/ko.json +++ b/homeassistant/components/rachio/translations/ko.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "Rachio \uacc4\uc815\uc758 API \ud0a4." + "api_key": "API \ud0a4" }, "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc815 \uc124\uc815\uc744 \uc120\ud0dd\ud55c \ub2e4\uc74c 'GET API KEY ' \ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", "title": "Rachio \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json new file mode 100644 index 00000000000..2173c768185 --- /dev/null +++ b/homeassistant/components/rachio/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "De API-sleutel voor het Rachio-account." + }, + "description": "U heeft de API-sleutel nodig van https://app.rach.io/. Selecteer 'Accountinstellingen en klik vervolgens op' GET API KEY '.", + "title": "Maak verbinding met uw Rachio-apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Hoe lang, in minuten, om een station in te schakelen wanneer de schakelaar is ingeschakeld." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/no.json b/homeassistant/components/rachio/translations/no.json index 84872e89434..063b1159056 100644 --- a/homeassistant/components/rachio/translations/no.json +++ b/homeassistant/components/rachio/translations/no.json @@ -13,7 +13,7 @@ "data": { "api_key": "API-n\u00f8kkelen for Rachio-kontoen." }, - "description": "Du trenger API-n\u00f8kkelen fra https://app.rach.io/. Velg \"Kontoinnstillinger\", og klikk deretter p\u00e5 \"GET API KEY\".", + "description": "Du trenger API-n\u00f8kkelen fra https://app.rach.io/. Velg 'Kontoinnstillinger' og klikk deretter p\u00e5 'F\u00e5 API n\u00f8kkel'.", "title": "Koble til Rachio-enheten din" } } diff --git a/homeassistant/components/rachio/translations/pl.json b/homeassistant/components/rachio/translations/pl.json index 9d68b88d015..db3fb0466cb 100644 --- a/homeassistant/components/rachio/translations/pl.json +++ b/homeassistant/components/rachio/translations/pl.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "api_key": "Klucz API dla konta Rachio." + "api_key": "[%key_id:common::config_flow::data::api_key%] dla konta Rachio." }, "description": "B\u0119dziesz potrzebowa\u0142 klucza API ze strony https://app.rach.io/. Wybierz 'Account Settings', a nast\u0119pnie kliknij 'GET API KEY'.", "title": "Po\u0142\u0105czenie z urz\u0105dzeniem Rachio" diff --git a/homeassistant/components/rachio/translations/ru.json b/homeassistant/components/rachio/translations/ru.json index fce3d6d3a14..f6fe53b5f8d 100644 --- a/homeassistant/components/rachio/translations/ru.json +++ b/homeassistant/components/rachio/translations/ru.json @@ -11,9 +11,9 @@ "step": { "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Rachio" + "api_key": "\u041a\u043b\u044e\u0447 API" }, - "description": "\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0443\u0436\u0435\u043d \u043a\u043b\u044e\u0447 API \u043e\u0442 https://app.rach.io/. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Account Settings', \u0430 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 'GET API KEY'.", + "description": "\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0443\u0436\u0435\u043d \u043a\u043b\u044e\u0447 API \u0441 \u0441\u0430\u0439\u0442\u0430 https://app.rach.io/. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Account Settings', \u0430 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 'GET API KEY'.", "title": "Rachio" } } diff --git a/homeassistant/components/rachio/translations/sv.json b/homeassistant/components/rachio/translations/sv.json new file mode 100644 index 00000000000..726912f62f9 --- /dev/null +++ b/homeassistant/components/rachio/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index 59e5115e2e6..7f05aa218e7 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "Rachio \u5e33\u865f API \u91d1\u9470\u3002" + "api_key": "API \u5bc6\u9470" }, "description": "\u5c07\u6703\u9700\u8981\u7531 https://app.rach.io/ \u53d6\u5f97 App \u5bc6\u9470\u3002\u9078\u64c7\u5e33\u865f\u8a2d\u5b9a\uff08Account Settings\uff09\u3001\u4e26\u9078\u64c7\u7372\u5f97\u5bc6\u9470\uff08GET API KEY\uff09\u3002", "title": "\u9023\u7dda\u81f3 Rachio \u8a2d\u5099" diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index a5960b8b28b..a3f95d5a5f3 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -15,6 +15,7 @@ from .const import ( KEY_EXTERNAL_ID, KEY_TYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_RAIN_DELAY_UPDATE, SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, ) @@ -30,6 +31,9 @@ SUBTYPE_SLEEP_MODE_OFF = "SLEEP_MODE_OFF" SUBTYPE_BROWNOUT_VALVE = "BROWNOUT_VALVE" SUBTYPE_RAIN_SENSOR_DETECTION_ON = "RAIN_SENSOR_DETECTION_ON" SUBTYPE_RAIN_SENSOR_DETECTION_OFF = "RAIN_SENSOR_DETECTION_OFF" + +# Rain delay values +TYPE_RAIN_DELAY_STATUS = "RAIN_DELAY" SUBTYPE_RAIN_DELAY_ON = "RAIN_DELAY_ON" SUBTYPE_RAIN_DELAY_OFF = "RAIN_DELAY_OFF" @@ -55,6 +59,7 @@ SUBTYPE_ZONE_CYCLING_COMPLETED = "ZONE_CYCLING_COMPLETED" LISTEN_EVENT_TYPES = [ "DEVICE_STATUS_EVENT", "ZONE_STATUS_EVENT", + "RAIN_DELAY_EVENT", "SCHEDULE_STATUS_EVENT", ] WEBHOOK_CONST_ID = "homeassistant.rachio:" @@ -62,6 +67,7 @@ WEBHOOK_PATH = URL_API + DOMAIN SIGNAL_MAP = { TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, + TYPE_RAIN_DELAY_STATUS: SIGNAL_RACHIO_RAIN_DELAY_UPDATE, TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, } diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index acbd6fe7e5e..c5e7f2f956e 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -4,7 +4,7 @@ import logging import radiotherm import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -125,7 +125,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(tstats, True) -class RadioThermostat(ClimateDevice): +class RadioThermostat(ClimateEntity): """Representation of a Radio Thermostat.""" def __init__(self, device, hold_temp): diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 51c5f7a9dbe..62c6824f5e0 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -3,7 +3,7 @@ import logging from pyrainbird import RainbirdController -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import ( DATA_RAINBIRD, @@ -27,7 +27,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class RainBirdSensor(BinarySensorDevice): +class RainBirdSensor(BinarySensorEntity): """A sensor implementation for Rain Bird device.""" def __init__(self, controller: RainbirdController, sensor_type): diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 7f589401e3c..1fd162c07b7 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -5,7 +5,7 @@ import logging from pyrainbird import AvailableStations, RainbirdController import voluptuous as vol -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME from homeassistant.helpers import config_validation as cv @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class RainBirdSwitch(SwitchDevice): +class RainBirdSwitch(SwitchEntity): """Representation of a Rain Bird switch.""" def __init__(self, controller: RainbirdController, zone, time, name): diff --git a/homeassistant/components/raincloud/binary_sensor.py b/homeassistant/components/raincloud/binary_sensor.py index 2074a57df98..d2659e133b0 100644 --- a/homeassistant/components/raincloud/binary_sensor.py +++ b/homeassistant/components/raincloud/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -41,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice): +class RainCloudBinarySensor(RainCloudEntity, BinarySensorEntity): """A sensor implementation for raincloud device.""" @property diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index f85ed884ca9..d6733412cac 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -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 ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class RainCloudSwitch(RainCloudEntity, SwitchDevice): +class RainCloudSwitch(RainCloudEntity, SwitchEntity): """A switch implementation for raincloud device.""" def __init__(self, default_watering_timer, *args): diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index e5fdc8d6b46..b3f7ffb7d6b 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -1,7 +1,7 @@ """This platform provides binary sensors for key RainMachine data.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -86,7 +86,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): +class RainMachineBinarySensor(RainMachineEntity, BinarySensorEntity): """A sensor implementation for raincloud device.""" def __init__( diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 0d1eabc1cde..555230d1f0f 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -5,8 +5,8 @@ "title": "Fill in your information", "data": { "ip_address": "Hostname or IP Address", - "password": "Password", - "port": "Port" + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -18,4 +18,4 @@ "already_configured": "This RainMachine controller is already configured." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index b93f607f853..936aa8f1f0e 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -4,7 +4,7 @@ import logging from regenmaschine.errors import RequestError -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ATTR_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -111,7 +111,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities, True) -class RainMachineSwitch(RainMachineEntity, SwitchDevice): +class RainMachineSwitch(RainMachineEntity, SwitchEntity): """A class to represent a generic RainMachine switch.""" def __init__(self, rainmachine, switch_data): diff --git a/homeassistant/components/rainmachine/translations/es-419.json b/homeassistant/components/rainmachine/translations/es-419.json index 73da8c7c1d4..0767c509bf9 100644 --- a/homeassistant/components/rainmachine/translations/es-419.json +++ b/homeassistant/components/rainmachine/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Este controlador RainMachine ya est\u00e1 configurado." + }, "error": { "identifier_exists": "Cuenta ya registrada", "invalid_credentials": "Credenciales no v\u00e1lidas" diff --git a/homeassistant/components/rainmachine/translations/fi.json b/homeassistant/components/rainmachine/translations/fi.json new file mode 100644 index 00000000000..61c91d5e97d --- /dev/null +++ b/homeassistant/components/rainmachine/translations/fi.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Tili on jo rekister\u00f6ity", + "invalid_credentials": "Virheelliset tunnistetiedot" + }, + "step": { + "user": { + "data": { + "ip_address": "Palvelin tai IP-osoite", + "password": "Salasana", + "port": "Portti" + }, + "title": "T\u00e4yt\u00e4 tietosi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/ko.json b/homeassistant/components/rainmachine/translations/ko.json index 30d7fc10979..981849cee7b 100644 --- a/homeassistant/components/rainmachine/translations/ko.json +++ b/homeassistant/components/rainmachine/translations/ko.json @@ -14,7 +14,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30" } } } diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 545d0ded465..3d13a48712b 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Deze RainMachine controller is al geconfigureerd." + }, "error": { "identifier_exists": "Account bestaat al", "invalid_credentials": "Ongeldige gebruikersgegevens" diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index bc80cdedb31..cf031e13f10 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -12,7 +12,7 @@ "data": { "ip_address": "Vertsnavn eller IP-adresse", "password": "Passord", - "port": "" + "port": "Port" }, "title": "Fyll ut informasjonen din" } diff --git a/homeassistant/components/rainmachine/translations/pl.json b/homeassistant/components/rainmachine/translations/pl.json index 8ea3ab6dbc4..20a016755b2 100644 --- a/homeassistant/components/rainmachine/translations/pl.json +++ b/homeassistant/components/rainmachine/translations/pl.json @@ -10,9 +10,9 @@ "step": { "user": { "data": { - "ip_address": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "port": "Port" + "ip_address": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]" }, "title": "Wprowad\u017a dane" } diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index e502439b28c..baec29c5937 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -7,7 +7,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 @@ -32,7 +32,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([RandomSensor(name, device_class)], True) -class RandomSensor(BinarySensorDevice): +class RandomSensor(BinarySensorEntity): """Representation of a Random binary sensor.""" def __init__(self, name, device_class): diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py index 6a88318706d..ea3130da4ba 100644 --- a/homeassistant/components/raspihats/binary_sensor.py +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE_CLASS, @@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors) -class I2CHatBinarySensor(BinarySensorDevice): +class I2CHatBinarySensor(BinarySensorEntity): """Representation of a binary sensor that uses a I2C-HAT digital input.""" I2C_HATS_MANAGER = None diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index ec07119b96a..cd4e28e1f10 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -12,7 +12,7 @@ from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constan from raspyrfm_client.device_implementations.manufacturer_constants import Manufacturer 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_HOST, CONF_NAME, @@ -87,7 +87,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switch_entities) -class RaspyRFMSwitch(SwitchDevice): +class RaspyRFMSwitch(SwitchEntity): """Representation of a RaspyRFM switch.""" def __init__(self, raspyrfm_client, name: str, gateway, controlunit): diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index c242f23dfdd..9c6ac8f9508 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -5,7 +5,7 @@ import logging from pyrecswitch import RSNetwork, RSNetworkError 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_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([RecSwitchSwitch(device, device_name, mac_address)]) -class RecSwitchSwitch(SwitchDevice): +class RecSwitchSwitch(SwitchEntity): """Representation of a recswitch device.""" def __init__(self, device, device_name, mac_address): diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 580c0a3b152..ca9de0bfb62 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -122,7 +122,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) -class RemoteDevice(ToggleEntity): +class RemoteEntity(ToggleEntity): """Representation of a remote.""" @property @@ -149,3 +149,15 @@ class RemoteDevice(ToggleEntity): """Learn a command from a device.""" assert self.hass is not None await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs)) + + +class RemoteDevice(RemoteEntity): + """Representation of a remote (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "RemoteDevice is deprecated, modify %s to extend RemoteEntity", + cls.__name__, + ) diff --git a/homeassistant/components/remote/translations/no.json b/homeassistant/components/remote/translations/no.json index ad3dec70b8f..2e65d515e59 100644 --- a/homeassistant/components/remote/translations/no.json +++ b/homeassistant/components/remote/translations/no.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, "title": "Fjernkontroll" } \ No newline at end of file diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 862bd30ae43..d97183de86e 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv @@ -57,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class RemoteRPiGPIOBinarySensor(BinarySensorDevice): +class RemoteRPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses a Remote Raspberry Pi GPIO.""" def __init__(self, name, button, invert_logic): diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 1bbde240789..6d797dfd834 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import ( CONF_AUTHENTICATION, @@ -91,7 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([RestBinarySensor(hass, rest, name, device_class, value_template)]) -class RestBinarySensor(BinarySensorDevice): +class RestBinarySensor(BinarySensorEntity): """Representation of a REST binary sensor.""" def __init__(self, hass, rest, name, device_class, value_template): diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 84d503bb09e..ab880201072 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,7 +6,7 @@ import aiohttp import async_timeout 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_HEADERS, CONF_METHOD, @@ -109,7 +109,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.error("No route to resource/endpoint: %s", resource) -class RestSwitch(SwitchDevice): +class RestSwitch(SwitchEntity): """Representation of a switch that can be toggled using REST.""" def __init__( diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 542c63f3b7a..68b6d841654 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -230,7 +230,7 @@ async def async_setup(hass, config): ) try: - with async_timeout.timeout(CONNECTION_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(CONNECTION_TIMEOUT): transport, protocol = await connection except ( diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index c3600da4051..8962af3c0d4 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -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_FORCE_UPDATE, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -56,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices_from_config(config)) -class RflinkBinarySensor(RflinkDevice, BinarySensorDevice): +class RflinkBinarySensor(RflinkDevice, BinarySensorEntity): """Representation of an Rflink binary sensor.""" def __init__( diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 794542cb9d4..5eacce3afa8 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_OPEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -112,7 +112,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices_from_config(config)) -class RflinkCover(RflinkCommand, CoverDevice, RestoreEntity): +class RflinkCover(RflinkCommand, CoverEntity, RestoreEntity): """Rflink entity which can switch on/stop/off (eg: cover).""" async def async_added_to_hass(self): diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 348fff0da9a..65d6acb3de9 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -7,7 +7,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_NAME, CONF_TYPE import homeassistant.helpers.config_validation as cv @@ -159,11 +159,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_COMMAND] = add_new_device -class RflinkLight(SwitchableRflinkDevice, Light): +class RflinkLight(SwitchableRflinkDevice, LightEntity): """Representation of a Rflink light.""" -class DimmableRflinkLight(SwitchableRflinkDevice, Light): +class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): """Rflink light device that support dimming.""" _brightness = 255 @@ -208,7 +208,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light): return SUPPORT_BRIGHTNESS -class HybridRflinkLight(SwitchableRflinkDevice, Light): +class HybridRflinkLight(SwitchableRflinkDevice, LightEntity): """Rflink light device that sends out both dim and on/off commands. Used for protocols which support lights that are not exclusively on/off @@ -271,7 +271,7 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): return SUPPORT_BRIGHTNESS -class ToggleRflinkLight(SwitchableRflinkDevice, Light): +class ToggleRflinkLight(SwitchableRflinkDevice, LightEntity): """Rflink light device which sends out only 'on' commands. Some switches like for example Livolo light switches use the diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index 83c335f0f03..266f9a4625e 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -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 @@ -69,5 +69,5 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices_from_config(config)) -class RflinkSwitch(SwitchableRflinkDevice, SwitchDevice): +class RflinkSwitch(SwitchableRflinkDevice, SwitchEntity): """Representation of a Rflink switch.""" diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index a88594dccea..5e610128ea6 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import ( CONF_COMMAND_OFF, @@ -171,7 +171,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) -class RfxtrxBinarySensor(BinarySensorDevice): +class RfxtrxBinarySensor(BinarySensorEntity): """A representation of a RFXtrx binary sensor.""" def __init__( diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index da19c42ed69..bd64d20fe46 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,7 +2,7 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import CONF_NAME, STATE_OPEN from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -63,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -class RfxtrxCover(RfxtrxDevice, CoverDevice, RestoreEntity): +class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): """Representation of a RFXtrx cover.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 437cce89c49..ea6c834f63b 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): RECEIVED_EVT_SUBSCRIBERS.append(light_update) -class RfxtrxLight(RfxtrxDevice, Light, RestoreEntity): +class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): """Representation of a RFXtrx light.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 05e4a37ab44..960dc7dd33a 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -4,7 +4,7 @@ import logging import RFXtrx as rfxtrxmod 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, STATE_ON from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): RECEIVED_EVT_SUBSCRIBERS.append(switch_update) -class RfxtrxSwitch(RfxtrxDevice, SwitchDevice, RestoreEntity): +class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): """Representation of a RFXtrx switch.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 385d8a4f955..fa303b94378 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from . import DOMAIN @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class RingBinarySensor(RingEntityMixin, BinarySensorDevice): +class RingBinarySensor(RingEntityMixin, BinarySensorEntity): """A binary sensor implementation for Ring device.""" _active_alert = None diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 3340e0ad603..8a5b75cfecd 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -4,7 +4,7 @@ import logging import requests -from homeassistant.components.light import Light +from homeassistant.components.light import LightEntity from homeassistant.core import callback import homeassistant.util.dt as dt_util @@ -38,7 +38,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(lights) -class RingLight(RingEntityMixin, Light): +class RingLight(RingEntityMixin, LightEntity): """Creates a switch to turn the ring cameras light on and off.""" def __init__(self, config_entry_id, device): diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index 8c8c7b1e6ab..04b19b66614 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -3,17 +3,24 @@ "step": { "user": { "title": "Sign-in with Ring account", - "data": { "username": "Username", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } }, "2fa": { "title": "Two-factor authentication", - "data": { "2fa": "Two-factor code" } + "data": { + "2fa": "Two-factor code" + } } }, "error": { "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index e2f1882adf6..09627b50965 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,7 +4,7 @@ import logging import requests -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback import homeassistant.util.dt as dt_util @@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(switches) -class BaseRingSwitch(RingEntityMixin, SwitchDevice): +class BaseRingSwitch(RingEntityMixin, SwitchEntity): """Represents a switch for controlling an aspect of a ring device.""" def __init__(self, config_entry_id, device, device_type): diff --git a/homeassistant/components/ring/translations/es-419.json b/homeassistant/components/ring/translations/es-419.json new file mode 100644 index 00000000000..331769fbb4c --- /dev/null +++ b/homeassistant/components/ring/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dos factores" + }, + "title": "Autenticaci\u00f3n de dos factores" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Iniciar sesi\u00f3n con cuenta de Ring" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/ko.json b/homeassistant/components/ring/translations/ko.json index caf6d824d0d..c1e93133f91 100644 --- a/homeassistant/components/ring/translations/ko.json +++ b/homeassistant/components/ring/translations/ko.json @@ -19,7 +19,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "Ring \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778" + "title": "Ring \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778\ud558\uae30" } } } diff --git a/homeassistant/components/ring/translations/no.json b/homeassistant/components/ring/translations/no.json index 1a64f0268a6..566568ce37c 100644 --- a/homeassistant/components/ring/translations/no.json +++ b/homeassistant/components/ring/translations/no.json @@ -12,7 +12,7 @@ "data": { "2fa": "To-faktorskode" }, - "title": "To-faktor autentisering" + "title": "Totrinnsbekreftelse" }, "user": { "data": { diff --git a/homeassistant/components/ring/translations/pl.json b/homeassistant/components/ring/translations/pl.json index 319348aa8a2..d6f427aab4c 100644 --- a/homeassistant/components/ring/translations/pl.json +++ b/homeassistant/components/ring/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Niespodziewany b\u0142\u0105d" + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "2fa": { @@ -16,8 +16,8 @@ }, "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Zaloguj si\u0119 do konta Ring" } diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 636260b510c..ef233f64a1b 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,22 +1,32 @@ """Support for Roku.""" import asyncio from datetime import timedelta -from socket import gaierror as SocketGIAError -from typing import Dict +import logging +from typing import Any, Dict -from requests.exceptions import RequestException -from roku import Roku, RokuException +from rokuecp import Roku, RokuError +from rokuecp.models import Device import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant +from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import utcnow -from .const import DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + DOMAIN, +) CONFIG_SCHEMA = vol.Schema( { @@ -28,21 +38,11 @@ CONFIG_SCHEMA = vol.Schema( ) PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=20) +_LOGGER = logging.getLogger(__name__) -def get_roku_data(host: str) -> dict: - """Retrieve a Roku instance and version info for the device.""" - roku = Roku(host) - roku_device_info = roku.device_info - - return { - DATA_CLIENT: roku, - DATA_DEVICE_INFO: roku_device_info, - } - - -async def async_setup(hass: HomeAssistant, config: Dict) -> bool: +async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the Roku integration.""" hass.data.setdefault(DOMAIN, {}) @@ -57,16 +57,15 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" - try: - roku_data = await hass.async_add_executor_job( - get_roku_data, entry.data[CONF_HOST], - ) - except (SocketGIAError, RequestException, RokuException) as exception: - raise ConfigEntryNotReady from exception + coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + await coordinator.async_refresh() - hass.data[DOMAIN][entry.entry_id] = roku_data + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = coordinator for component in PLATFORMS: hass.async_create_task( @@ -76,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -91,3 +90,87 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class RokuDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Roku data.""" + + def __init__( + self, hass: HomeAssistantType, *, host: str, + ): + """Initialize global Roku data updater.""" + self.roku = Roku(host=host, session=async_get_clientsession(hass)) + + self.full_update_interval = timedelta(minutes=15) + self.last_full_update = None + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> Device: + """Fetch data from Roku.""" + full_update = self.last_full_update is None or utcnow() >= ( + self.last_full_update + self.full_update_interval + ) + + try: + data = await self.roku.update(full_update=full_update) + + if full_update: + self.last_full_update = utcnow() + + return data + except RokuError as error: + raise UpdateFailed(f"Invalid response from API: {error}") + + +class RokuEntity(Entity): + """Defines a base Roku entity.""" + + def __init__( + self, *, device_id: str, name: str, coordinator: RokuDataUpdateCoordinator + ) -> None: + """Initialize the Roku entity.""" + self._device_id = device_id + self._name = name + self.coordinator = coordinator + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self) -> None: + """Update an Roku entity.""" + await self.coordinator.async_request_refresh() + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this Roku device.""" + if self._device_id is None: + return None + + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: self.name, + ATTR_MANUFACTURER: self.coordinator.data.info.brand, + ATTR_MODEL: self.coordinator.data.info.model_name, + ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + } diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index eec5683c95d..27ab63c728b 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -1,11 +1,9 @@ """Config flow for Roku.""" import logging -from socket import gaierror as SocketGIAError from typing import Any, Dict, Optional from urllib.parse import urlparse -from requests.exceptions import RequestException -from roku import Roku, RokuException +from rokuecp import Roku, RokuError import voluptuous as vol from homeassistant.components.ssdp import ( @@ -16,7 +14,8 @@ from homeassistant.components.ssdp import ( from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN # pylint: disable=unused-import @@ -28,24 +27,18 @@ ERROR_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) -def validate_input(data: Dict) -> Dict: +async def validate_input(hass: HomeAssistantType, data: Dict) -> Dict: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - - try: - roku = Roku(data["host"]) - device_info = roku.device_info - except (SocketGIAError, RequestException, RokuException) as exception: - raise CannotConnect from exception - except Exception as exception: # pylint: disable=broad-except - raise UnknownError from exception + session = async_get_clientsession(hass) + roku = Roku(data[CONF_HOST], session=session) + device = await roku.update() return { - "title": data["host"], - "host": data["host"], - "serial_num": device_info.serial_num, + "title": device.info.name, + "serial_number": device.info.serial_number, } @@ -55,6 +48,10 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + def __init__(self): + """Set up the instance.""" + self.discovery_info = {} + @callback def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: """Show the form to the user.""" @@ -78,16 +75,17 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} try: - info = await self.hass.async_add_executor_job(validate_input, user_input) - except CannotConnect: + info = await validate_input(self.hass, user_input) + except RokuError: + _LOGGER.debug("Roku Error", exc_info=True) errors["base"] = ERROR_CANNOT_CONNECT return self._show_form(errors) - except UnknownError: + except Exception: # pylint: disable=broad-except _LOGGER.exception("Unknown error trying to connect") return self.async_abort(reason=ERROR_UNKNOWN) - await self.async_set_unique_id(info["serial_num"]) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(info["serial_number"]) + self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) return self.async_create_entry(title=info["title"], data=user_input) @@ -97,15 +95,24 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] - serial_num = discovery_info[ATTR_UPNP_SERIAL] + serial_number = discovery_info[ATTR_UPNP_SERIAL] - await self.async_set_unique_id(serial_num) + await self.async_set_unique_id(serial_number) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context.update( - {CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": host}} - ) + self.context.update({"title_placeholders": {"name": name}}) + + self.discovery_info.update({CONF_HOST: host, CONF_NAME: name}) + + try: + await validate_input(self.hass, self.discovery_info) + except RokuError: + _LOGGER.debug("Roku Error", exc_info=True) + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error trying to connect") + return self.async_abort(reason=ERROR_UNKNOWN) return await self.async_step_ssdp_confirm() @@ -114,30 +121,13 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): ) -> Dict[str, Any]: """Handle user-confirmation of discovered device.""" # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - name = self.context.get(CONF_NAME) + if user_input is None: + return self.async_show_form( + step_id="ssdp_confirm", + description_placeholders={"name": self.discovery_info[CONF_NAME]}, + errors={}, + ) - if user_input is not None: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - user_input[CONF_HOST] = self.context.get(CONF_HOST) - user_input[CONF_NAME] = name - - try: - await self.hass.async_add_executor_job(validate_input, user_input) - return self.async_create_entry(title=name, data=user_input) - except CannotConnect: - return self.async_abort(reason=ERROR_CANNOT_CONNECT) - except UnknownError: - _LOGGER.exception("Unknown error trying to connect") - return self.async_abort(reason=ERROR_UNKNOWN) - - return self.async_show_form( - step_id="ssdp_confirm", description_placeholders={"name": name}, + return self.async_create_entry( + title=self.discovery_info[CONF_NAME], data=self.discovery_info, ) - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class UnknownError(HomeAssistantError): - """Error to indicate we encountered an unknown error.""" diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index b06eed5df9f..dc51e5d6f9b 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -1,8 +1,11 @@ """Constants for the Roku integration.""" DOMAIN = "roku" -DATA_CLIENT = "client" -DATA_DEVICE_INFO = "device_info" +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_SOFTWARE_VERSION = "sw_version" +# Default Values DEFAULT_PORT = 8060 -DEFAULT_MANUFACTURER = "Roku" diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index e0c7c9f5c49..62b3cc58fc8 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["roku==4.1.0"], + "requirements": ["rokuecp==0.4.0"], "ssdp": [ { "st": "roku:ecp", diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 950946831a8..7b64888bbd1 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,14 +1,10 @@ """Support for the Roku media player.""" import logging +from typing import List -from requests.exceptions import ( - ConnectionError as RequestsConnectionError, - ReadTimeout as RequestsReadTimeout, -) -from roku import RokuException - -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PLAY, @@ -18,18 +14,19 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, ) from homeassistant.const import STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_STANDBY -from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DEFAULT_PORT, DOMAIN +from . import RokuDataUpdateCoordinator, RokuEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) SUPPORT_ROKU = ( SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY @@ -41,68 +38,48 @@ SUPPORT_ROKU = ( async def async_setup_entry(hass, entry, async_add_entities): """Set up the Roku config entry.""" - roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] - async_add_entities([RokuDevice(roku)], True) + coordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = coordinator.data.info.serial_number + async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) -class RokuDevice(MediaPlayerDevice): - """Representation of a Roku device on the network.""" +class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): + """Representation of a Roku media player on the network.""" - def __init__(self, roku): + def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None: """Initialize the Roku device.""" - self.roku = roku - self.ip_address = roku.host - self.channels = [] - self.current_app = None - self._available = False - self._device_info = {} - self._power_state = "Unknown" + super().__init__( + coordinator=coordinator, + name=coordinator.data.info.name, + device_id=unique_id, + ) - def update(self): - """Retrieve latest state.""" - try: - self._device_info = self.roku.device_info - self._power_state = self.roku.power_state - self.ip_address = self.roku.host - self.channels = self.get_source_list() - self.current_app = self.roku.current_app - self._available = True - except (RequestsConnectionError, RequestsReadTimeout, RokuException): - self._available = False - - def get_source_list(self): - """Get the list of applications to be used as sources.""" - return ["Home"] + sorted(channel.name for channel in self.roku.apps) + self._unique_id = unique_id @property - def should_poll(self): - """Device should be polled.""" - return True + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self._unique_id @property - def name(self): - """Return the name of the device.""" - if self._device_info.user_device_name: - return self._device_info.user_device_name - - return f"Roku {self._device_info.serial_num}" - - @property - def state(self): + def state(self) -> str: """Return the state of the device.""" - if self._power_state == "Off": + if self.coordinator.data.state.standby: return STATE_STANDBY - if self.current_app is None: + if self.coordinator.data.app is None: return None - if self.current_app.name == "Power Saver" or self.current_app.is_screensaver: + if ( + self.coordinator.data.app.name == "Power Saver" + or self.coordinator.data.app.screensaver + ): return STATE_IDLE - if self.current_app.name == "Roku": + if self.coordinator.data.app.name == "Roku": return STATE_HOME - if self.current_app.name is not None: + if self.coordinator.data.app.name is not None: return STATE_PLAYING return None @@ -113,109 +90,108 @@ class RokuDevice(MediaPlayerDevice): return SUPPORT_ROKU @property - def available(self): - """Return if able to retrieve information from device or not.""" - return self._available - - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._device_info.serial_num - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(DOMAIN, self.unique_id)}, - "manufacturer": DEFAULT_MANUFACTURER, - "model": self._device_info.model_num, - "sw_version": self._device_info.software_version, - } - - @property - def media_content_type(self): + def media_content_type(self) -> str: """Content type of current playing media.""" - if self.current_app is None or self.current_app.name in ("Power Saver", "Roku"): + if self.app_id is None or self.app_name in ("Power Saver", "Roku"): return None - return MEDIA_TYPE_CHANNEL + if self.app_id == "tvinput.dtv" and self.coordinator.data.channel is not None: + return MEDIA_TYPE_CHANNEL + + return MEDIA_TYPE_APP @property - def media_image_url(self): + def media_image_url(self) -> str: """Image url of current playing media.""" - if self.current_app is None or self.current_app.name in ("Power Saver", "Roku"): + if self.app_id is None or self.app_name in ("Power Saver", "Roku"): return None - if self.current_app.id is None: - return None - - return ( - f"http://{self.ip_address}:{DEFAULT_PORT}/query/icon/{self.current_app.id}" - ) + return self.coordinator.roku.app_icon_url(self.app_id) @property - def app_name(self): + def app_name(self) -> str: """Name of the current running app.""" - if self.current_app is not None: - return self.current_app.name + if self.coordinator.data.app is not None: + return self.coordinator.data.app.name + + return None @property - def app_id(self): + def app_id(self) -> str: """Return the ID of the current running app.""" - if self.current_app is not None: - return self.current_app.id + if self.coordinator.data.app is not None: + return self.coordinator.data.app.app_id + + return None @property - def source(self): + def media_channel(self): + """Return the TV channel currently tuned.""" + if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None: + return None + + if self.coordinator.data.channel.name is not None: + return f"{self.coordinator.data.channel.name} ({self.coordinator.data.channel.number})" + + return self.coordinator.data.channel.number + + @property + def media_title(self): + """Return the title of current playing media.""" + if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None: + return None + + if self.coordinator.data.channel.program_title is not None: + return self.coordinator.data.channel.program_title + + return None + + @property + def source(self) -> str: """Return the current input source.""" - if self.current_app is not None: - return self.current_app.name + if self.coordinator.data.app is not None: + return self.coordinator.data.app.name + + return None @property - def source_list(self): + def source_list(self) -> List: """List of available input sources.""" - return self.channels + return ["Home"] + sorted(app.name for app in self.coordinator.data.apps) - def turn_on(self): + async def async_turn_on(self) -> None: """Turn on the Roku.""" - self.roku.poweron() + await self.coordinator.roku.remote("poweron") - def turn_off(self): + async def async_turn_off(self) -> None: """Turn off the Roku.""" - self.roku.poweroff() + await self.coordinator.roku.remote("poweroff") - def media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send play/pause command.""" - if self.current_app is not None: - self.roku.play() + await self.coordinator.roku.remote("play") - def media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" - if self.current_app is not None: - self.roku.reverse() + await self.coordinator.roku.remote("reverse") - def media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" - if self.current_app is not None: - self.roku.forward() + await self.coordinator.roku.remote("forward") - def mute_volume(self, mute): + async def async_mute_volume(self, mute) -> None: """Mute the volume.""" - if self.current_app is not None: - self.roku.volume_mute() + await self.coordinator.roku.remote("volume_mute") - def volume_up(self): + async def async_volume_up(self) -> None: """Volume up media player.""" - if self.current_app is not None: - self.roku.volume_up() + await self.coordinator.roku.remote("volume_up") - def volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" - if self.current_app is not None: - self.roku.volume_down() + await self.coordinator.roku.remote("volume_down") - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Tune to channel.""" if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error( @@ -225,16 +201,16 @@ class RokuDevice(MediaPlayerDevice): ) return - if self.current_app is not None: - self.roku.launch(self.roku["tvinput.dtv"], {"ch": media_id}) + await self.coordinator.roku.tune(media_id) - def select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" - if self.current_app is None: - return - if source == "Home": - self.roku.home() - else: - channel = self.roku[source] - channel.launch() + await self.coordinator.roku.remote("home") + + appl = next( + (app for app in self.coordinator.data.apps if app.name == source), None + ) + + if appl is not None: + await self.coordinator.roku.launch(appl.app_id) diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 9a61ec8d5d8..99e398fea68 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,17 +1,12 @@ """Support for the Roku remote.""" from typing import Callable, List -from requests.exceptions import ( - ConnectionError as RequestsConnectionError, - ReadTimeout as RequestsReadTimeout, -) -from roku import RokuException - -from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteDevice +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DOMAIN +from . import RokuDataUpdateCoordinator, RokuEntity +from .const import DOMAIN async def async_setup_entry( @@ -20,75 +15,46 @@ async def async_setup_entry( async_add_entities: Callable[[List, bool], None], ) -> bool: """Load Roku remote based on a config entry.""" - roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] - async_add_entities([RokuRemote(roku)], True) + coordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = coordinator.data.info.serial_number + async_add_entities([RokuRemote(unique_id, coordinator)], True) -class RokuRemote(RemoteDevice): +class RokuRemote(RokuEntity, RemoteEntity): """Device that sends commands to an Roku.""" - def __init__(self, roku): + def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None: """Initialize the Roku device.""" - self.roku = roku - self._available = False - self._device_info = {} + super().__init__( + device_id=unique_id, + name=coordinator.data.info.name, + coordinator=coordinator, + ) - def update(self): - """Retrieve latest state.""" - if not self.enabled: - return - - try: - self._device_info = self.roku.device_info - self._available = True - except (RequestsConnectionError, RequestsReadTimeout, RokuException): - self._available = False + self._unique_id = unique_id @property - def name(self): - """Return the name of the device.""" - if self._device_info.user_device_name: - return self._device_info.user_device_name - return f"Roku {self._device_info.serial_num}" + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self._unique_id @property - def available(self): - """Return if able to retrieve information from device or not.""" - return self._available - - @property - def unique_id(self): - """Return a unique ID.""" - return self._device_info.serial_num - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(DOMAIN, self.unique_id)}, - "manufacturer": DEFAULT_MANUFACTURER, - "model": self._device_info.model_num, - "sw_version": self._device_info.software_version, - } - - @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return True + return not self.coordinator.data.state.standby - @property - def should_poll(self): - """No polling needed for Roku.""" - return False + async def async_turn_on(self, **kwargs) -> None: + """Turn the device on.""" + await self.coordinator.roku.remote("poweron") - def send_command(self, command, **kwargs): + async def async_turn_off(self, **kwargs) -> None: + """Turn the device off.""" + await self.coordinator.roku.remote("poweroff") + + async def async_send_command(self, command: List, **kwargs) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] for _ in range(num_repeats): for single_command in command: - if not hasattr(self.roku, single_command): - continue - - getattr(self.roku, single_command)() + await self.coordinator.roku.remote(single_command) diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index bc73623ae31..6d9000b8669 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -4,7 +4,9 @@ "step": { "user": { "description": "Enter your Roku information.", - "data": { "host": "Host or IP address" } + "data": { + "host": "[%key:common::config_flow::data::host%]" + } }, "ssdp_confirm": { "title": "Roku", @@ -12,10 +14,12 @@ "data": {} } }, - "error": { "cannot_connect": "Failed to connect, please try again" }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { - "already_configured": "Roku device is already configured", - "unknown": "Unexpected error" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 07414cc2b3a..194705cc7cc 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Roku device is already configured", + "already_configured": "Device is already configured", "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again" + "cannot_connect": "Failed to connect" }, "flow_title": "Roku: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Host or IP address" + "host": "Host" }, "description": "Enter your Roku information.", "title": "Roku" diff --git a/homeassistant/components/roku/translations/es-419.json b/homeassistant/components/roku/translations/es-419.json new file mode 100644 index 00000000000..40a76670fe1 --- /dev/null +++ b/homeassistant/components/roku/translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo Roku ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "\u00bfDesea configurar {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "description": "Ingrese su informaci\u00f3n de Roku.", + "title": "Roku" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 64c3abf5d1f..e2fb99f1d2a 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -10,6 +10,10 @@ "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { + "data": { + "one": "Vide", + "other": "Vide" + }, "description": "Voulez-vous configurer {name} ?", "title": "Roku" }, diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json new file mode 100644 index 00000000000..ab0e6cbad74 --- /dev/null +++ b/homeassistant/components/roku/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "V\u00e1ratlan hiba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ko.json b/homeassistant/components/roku/translations/ko.json index 7420611936f..f6b4066948b 100644 --- a/homeassistant/components/roku/translations/ko.json +++ b/homeassistant/components/roku/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Roku \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Roku: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + "host": "\ud638\uc2a4\ud2b8" }, "description": "Roku \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "Roku" diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json new file mode 100644 index 00000000000..8f2fae7146e --- /dev/null +++ b/homeassistant/components/roku/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Roku-apparaat is al geconfigureerd", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Wilt u {name} instellen?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host- of IP-adres" + }, + "description": "Voer uw Roku-informatie in.", + "title": "Roku" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index 0f126e6decd..e96931bce47 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -11,14 +11,14 @@ "step": { "ssdp_confirm": { "description": "Vil du sette opp {name} ?", - "title": "Roku" + "title": "" }, "user": { "data": { "host": "Vert eller IP-adresse" }, - "description": "Skriv inn Roku-informasjonen din.", - "title": "Roku" + "description": "Fyll inn Roku-informasjonen din.", + "title": "" } } } diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index e1ad0604936..57f314126cb 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie Roku jest ju\u017c skonfigurowane.", - "unknown": "Niespodziewany b\u0142\u0105d." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." @@ -21,7 +21,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP" + "host": "[%key_id:common::config_flow::data::host%]" }, "description": "Wprowad\u017a dane Roku.", "title": "Roku" diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index 52a33805e63..3500aa96e04 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -5,7 +5,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "flow_title": "Roku: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + "host": "\u0425\u043e\u0441\u0442" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e Roku.", "title": "Roku" diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json new file mode 100644 index 00000000000..6d6f9223466 --- /dev/null +++ b/homeassistant/components/roku/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 5b310b20b63..b0599ce200a 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Roku \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "flow_title": "Roku\uff1a{name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u8f38\u5165 Roku \u8cc7\u8a0a\u3002", "title": "Roku" diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 90c7c06b387..5e7d47d2d57 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -40,15 +40,18 @@ def _has_all_unique_bilds(value): return value -DEVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_BLID): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, - vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, - }, +DEVICE_SCHEMA = vol.All( + cv.deprecated(CONF_CERT), + vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_BLID): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, + vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, + }, + ), ) diff --git a/homeassistant/components/roomba/binary_sensor.py b/homeassistant/components/roomba/binary_sensor.py index 49e3da1d8de..9acea9e4dbe 100644 --- a/homeassistant/components/roomba/binary_sensor.py +++ b/homeassistant/components/roomba/binary_sensor.py @@ -1,7 +1,7 @@ """Roomba binary sensor entities.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import roomba_reported_state from .const import BLID, DOMAIN, ROOMBA_SESSION @@ -21,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([roomba_vac], True) -class RoombaBinStatus(IRobotEntity, BinarySensorDevice): +class RoombaBinStatus(IRobotEntity, BinarySensorEntity): """Class to hold Roomba Sensor basic info.""" ICON = "mdi:delete-variant" @@ -42,7 +42,7 @@ class RoombaBinStatus(IRobotEntity, BinarySensorDevice): return self.ICON @property - def state(self): + def is_on(self): """Return the state of the sensor.""" return roomba_reported_state(self.vacuum).get("bin", {}).get("full", False) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 72ad6855d79..cffbb3de4c9 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -82,8 +82,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, user_input) except CannotConnect: errors = {"base": "cannot_connect"} - except Exception: # pylint: disable=broad-except - errors = {"base": "unknown"} if "base" not in errors: await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 2df5bf2abbf..f510f4965b0 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -19,7 +19,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATE, SUPPORT_STATUS, SUPPORT_STOP, - StateVacuumDevice, + StateVacuumEntity, ) from homeassistant.helpers.entity import Entity @@ -31,6 +31,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_CLEANING_TIME = "cleaning_time" ATTR_CLEANED_AREA = "cleaned_area" ATTR_ERROR = "error" +ATTR_ERROR_CODE = "error_code" ATTR_POSITION = "position" ATTR_SOFTWARE_VERSION = "software_version" @@ -113,7 +114,7 @@ class IRobotEntity(Entity): self.schedule_update_ha_state() -class IRobotVacuum(IRobotEntity, StateVacuumDevice): +class IRobotVacuum(IRobotEntity, StateVacuumEntity): """Base class for iRobot robots.""" def __init__(self, roomba, blid): @@ -174,11 +175,6 @@ class IRobotVacuum(IRobotEntity, StateVacuumDevice): # Roomba software version software_version = state.get("softwareVer") - # Error message in plain english - error_msg = "None" - if hasattr(self.vacuum, "error_message"): - error_msg = self.vacuum.error_message - # Set properties that are to appear in the GUI state_attrs = {ATTR_SOFTWARE_VERSION: software_version} @@ -198,9 +194,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumDevice): state_attrs[ATTR_CLEANING_TIME] = cleaning_time state_attrs[ATTR_CLEANED_AREA] = cleaned_area - # Skip error attr if there is none - if error_msg and error_msg != "None": - state_attrs[ATTR_ERROR] = error_msg + # Error + if self.vacuum.error_code != 0: + state_attrs[ATTR_ERROR] = self.vacuum.error_message + state_attrs[ATTR_ERROR_CODE] = self.vacuum.error_code # Not all Roombas expose position data # https://github.com/koalazak/dorita980/issues/48 diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 45fe9133bca..3d710467b58 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "iRobot Roomba", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.5.3"], + "requirements": ["roombapy==1.6.1"], "dependencies": [], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"] } diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index b813f35883b..d22a6e0509c 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -5,22 +5,26 @@ "title": "Connect to the device", "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "data": { - "host": "Hostname or IP Address", + "host": "[%key:common::config_flow::data::host%]", "blid": "BLID", - "password": "Password", + "password": "[%key:common::config_flow::data::password%]", "continuous": "Continuous", "delay": "Delay" } } }, "error": { - "unknown": "Unexpected error", "cannot_connect": "Failed to connect, please try again" } }, "options": { "step": { - "init": { "data": { "continuous": "Continuous", "delay": "Delay" } } + "init": { + "data": { + "continuous": "Continuous", + "delay": "Delay" + } + } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 2a9dcc9973a..a4afc71e5df 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -11,7 +11,7 @@ "certificate": "Certificate", "continuous": "Continuous", "delay": "Delay", - "host": "Hostname or IP Address", + "host": "Host", "password": "Password" }, "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roomba/translations/es-419.json b/homeassistant/components/roomba/translations/es-419.json new file mode 100644 index 00000000000..d452c82b112 --- /dev/null +++ b/homeassistant/components/roomba/translations/es-419.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "certificate": "Certificado", + "continuous": "Continuo", + "delay": "Retraso", + "host": "Nombre de host o direcci\u00f3n IP", + "password": "Contrase\u00f1a" + }, + "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Siga los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Conectarse al dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Continuo", + "delay": "Retraso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 0224ce76fcb..49d021bc198 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -7,10 +7,12 @@ "user": { "data": { "certificate": "Certificat", + "continuous": "En continu", "delay": "D\u00e9lai", "host": "Nom d'h\u00f4te ou adresse IP", "password": "Mot de passe" - } + }, + "title": "Se connecter \u00e0 l'appareil" } } }, @@ -18,6 +20,7 @@ "step": { "init": { "data": { + "continuous": "Continue", "delay": "D\u00e9lai" } } diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index f7278a6c43c..7990cd22bdb 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -11,7 +11,7 @@ "certificate": "\uc778\uc99d\uc11c", "continuous": "\uc5f0\uc18d", "delay": "\uc9c0\uc5f0", - "host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\ud604\uc7ac BLID \ubc0f \ube44\ubc00\ubc88\ud638\ub294 \uc218\ub3d9\uc73c\ub85c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \ub2e4\uc74c \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \uc808\ucc28\ub97c \ub530\ub77c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json new file mode 100644 index 00000000000..d49a9f488de --- /dev/null +++ b/homeassistant/components/roomba/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "certificate": "Certificaat", + "continuous": "Doorlopend", + "delay": "Vertraging", + "host": "Hostnaam of IP-adres", + "password": "Wachtwoord" + }, + "description": "Het ophalen van de BLID en het wachtwoord is momenteel een handmatig proces. Volg de stappen in de documentatie op: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Verbinding maken met het apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Doorlopend", + "delay": "Vertraging" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index 3772d6a08c5..0598a9ea247 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { @@ -11,8 +11,8 @@ "certificate": "Certyfikat", "continuous": "Ci\u0105g\u0142y", "delay": "Op\u00f3\u017anienie", - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o" + "host": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%]" }, "description": "Obecnie pobieranie BLID i has\u0142a jest procesem r\u0119cznym. Prosz\u0119 post\u0119powa\u0107 zgodnie z instrukcjami zawartymi w dokumentacji pod adresem: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials.", "title": "Po\u0142\u0105cz z urz\u0105dzeniem" diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index 2eaab685d20..fd8af88a9e2 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -9,9 +9,9 @@ "data": { "blid": "BLID", "certificate": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", - "continuous": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439", - "delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430", - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "continuous": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", + "delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 (\u0441\u0435\u043a.)", + "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json new file mode 100644 index 00000000000..46e0474cc50 --- /dev/null +++ b/homeassistant/components/roomba/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "certificate": "Certifikat", + "continuous": "Kontinuerlig", + "delay": "F\u00f6rdr\u00f6jning", + "host": "V\u00e4rdnamn eller IP-adress", + "password": "L\u00f6senord" + }, + "title": "Anslut till enheten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Kontinuerlig", + "delay": "F\u00f6rdr\u00f6jning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index b06d7d53f2a..13968bf7dda 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -11,7 +11,7 @@ "certificate": "\u8a8d\u8b49", "continuous": "\u9023\u7e8c", "delay": "\u5ef6\u9072", - "host": "\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, "description": "\u76ee\u524d\u63a5\u6536 BLID \u8207\u5bc6\u78bc\u70ba\u624b\u52d5\u904e\u7a0b\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1ahttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/rpi_camera/__init__.py b/homeassistant/components/rpi_camera/__init__.py index 04638e463a1..2f962872d8c 100644 --- a/homeassistant/components/rpi_camera/__init__.py +++ b/homeassistant/components/rpi_camera/__init__.py @@ -1 +1,89 @@ """The rpi_camera component.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_FILE_PATH, CONF_NAME +from homeassistant.helpers import config_validation as cv, discovery + +from .const import ( + CONF_HORIZONTAL_FLIP, + CONF_IMAGE_HEIGHT, + CONF_IMAGE_QUALITY, + CONF_IMAGE_ROTATION, + CONF_IMAGE_WIDTH, + CONF_OVERLAY_METADATA, + CONF_OVERLAY_TIMESTAMP, + CONF_TIMELAPSE, + CONF_VERTICAL_FLIP, + DEFAULT_HORIZONTAL_FLIP, + DEFAULT_IMAGE_HEIGHT, + DEFAULT_IMAGE_QUALITY, + DEFAULT_IMAGE_ROTATION, + DEFAULT_IMAGE_WIDTH, + DEFAULT_NAME, + DEFAULT_TIMELAPSE, + DEFAULT_VERTICAL_FLIP, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_FILE_PATH): cv.isfile, + vol.Optional( + CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), + vol.Optional( + CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT + ): vol.Coerce(int), + vol.Optional( + CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), + vol.Optional( + CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=359)), + vol.Optional(CONF_IMAGE_WIDTH, default=DEFAULT_IMAGE_WIDTH): vol.Coerce( + int + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OVERLAY_METADATA): vol.All( + vol.Coerce(int), vol.Range(min=4, max=2056) + ), + vol.Optional(CONF_OVERLAY_TIMESTAMP): cv.string, + vol.Optional(CONF_TIMELAPSE, default=DEFAULT_TIMELAPSE): vol.Coerce( + int + ), + vol.Optional( + CONF_VERTICAL_FLIP, default=DEFAULT_VERTICAL_FLIP + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the rpi_camera integration.""" + config_domain = config[DOMAIN] + hass.data[DOMAIN] = { + CONF_FILE_PATH: config_domain.get(CONF_FILE_PATH), + CONF_HORIZONTAL_FLIP: config_domain.get(CONF_HORIZONTAL_FLIP), + CONF_IMAGE_WIDTH: config_domain.get(CONF_IMAGE_WIDTH), + CONF_IMAGE_HEIGHT: config_domain.get(CONF_IMAGE_HEIGHT), + CONF_IMAGE_QUALITY: config_domain.get(CONF_IMAGE_QUALITY), + CONF_IMAGE_ROTATION: config_domain.get(CONF_IMAGE_ROTATION), + CONF_NAME: config_domain.get(CONF_NAME), + CONF_OVERLAY_METADATA: config_domain.get(CONF_OVERLAY_METADATA), + CONF_OVERLAY_TIMESTAMP: config_domain.get(CONF_OVERLAY_TIMESTAMP), + CONF_TIMELAPSE: config_domain.get(CONF_TIMELAPSE), + CONF_VERTICAL_FLIP: config_domain.get(CONF_VERTICAL_FLIP), + } + + discovery.load_platform(hass, "camera", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index bf04e0ef492..47ce87c4a8d 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -5,53 +5,24 @@ import shutil import subprocess from tempfile import NamedTemporaryFile -import voluptuous as vol - -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_HORIZONTAL_FLIP, + CONF_IMAGE_HEIGHT, + CONF_IMAGE_QUALITY, + CONF_IMAGE_ROTATION, + CONF_IMAGE_WIDTH, + CONF_OVERLAY_METADATA, + CONF_OVERLAY_TIMESTAMP, + CONF_TIMELAPSE, + CONF_VERTICAL_FLIP, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -CONF_HORIZONTAL_FLIP = "horizontal_flip" -CONF_IMAGE_HEIGHT = "image_height" -CONF_IMAGE_QUALITY = "image_quality" -CONF_IMAGE_ROTATION = "image_rotation" -CONF_IMAGE_WIDTH = "image_width" -CONF_TIMELAPSE = "timelapse" -CONF_VERTICAL_FLIP = "vertical_flip" - -DEFAULT_HORIZONTAL_FLIP = 0 -DEFAULT_IMAGE_HEIGHT = 480 -DEFAULT_IMAGE_QUALITY = 7 -DEFAULT_IMAGE_ROTATION = 0 -DEFAULT_IMAGE_WIDTH = 640 -DEFAULT_NAME = "Raspberry Pi Camera" -DEFAULT_TIMELAPSE = 1000 -DEFAULT_VERTICAL_FLIP = 0 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_FILE_PATH): cv.isfile, - vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP): vol.All( - vol.Coerce(int), vol.Range(min=0, max=1) - ), - vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT): vol.Coerce(int), - vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) - ), - vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION): vol.All( - vol.Coerce(int), vol.Range(min=0, max=359) - ), - vol.Optional(CONF_IMAGE_WIDTH, default=DEFAULT_IMAGE_WIDTH): vol.Coerce(int), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TIMELAPSE, default=1000): vol.Coerce(int), - vol.Optional(CONF_VERTICAL_FLIP, default=DEFAULT_VERTICAL_FLIP): vol.All( - vol.Coerce(int), vol.Range(min=0, max=1) - ), - } -) - def kill_raspistill(*args): """Kill any previously running raspistill process..""" @@ -62,24 +33,18 @@ def kill_raspistill(*args): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Raspberry Camera.""" + # We only want this platform to be set up via discovery. + # prevent initializing by erroneous platform config section in yaml conf + if discovery_info is None: + return + if shutil.which("raspistill") is None: _LOGGER.error("'raspistill' was not found") - return False - - setup_config = { - CONF_NAME: config.get(CONF_NAME), - CONF_IMAGE_WIDTH: config.get(CONF_IMAGE_WIDTH), - CONF_IMAGE_HEIGHT: config.get(CONF_IMAGE_HEIGHT), - CONF_IMAGE_QUALITY: config.get(CONF_IMAGE_QUALITY), - CONF_IMAGE_ROTATION: config.get(CONF_IMAGE_ROTATION), - CONF_TIMELAPSE: config.get(CONF_TIMELAPSE), - CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP), - CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP), - CONF_FILE_PATH: config.get(CONF_FILE_PATH), - } + return hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill) + setup_config = hass.data[DOMAIN] file_path = setup_config[CONF_FILE_PATH] def delete_temp_file(*args): @@ -100,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Check whether the file path has been whitelisted elif not hass.config.is_allowed_path(file_path): _LOGGER.error("'%s' is not a whitelisted directory", file_path) - return False + return add_entities([RaspberryCamera(setup_config)]) @@ -142,6 +107,16 @@ class RaspberryCamera(Camera): if device_info[CONF_VERTICAL_FLIP]: cmd_args.append("-vf") + if device_info[CONF_OVERLAY_METADATA]: + cmd_args.append("-a") + cmd_args.append(str(device_info[CONF_OVERLAY_METADATA])) + + if device_info[CONF_OVERLAY_TIMESTAMP]: + cmd_args.append("-a") + cmd_args.append("4") + cmd_args.append("-a") + cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP])) + subprocess.Popen(cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) def camera_image(self): @@ -153,3 +128,8 @@ class RaspberryCamera(Camera): def name(self): """Return the name of this camera.""" return self._name + + @property + def frame_interval(self): + """Return the interval between frames of the stream.""" + return self._config[CONF_TIMELAPSE] / 1000 diff --git a/homeassistant/components/rpi_camera/const.py b/homeassistant/components/rpi_camera/const.py new file mode 100644 index 00000000000..19da9e2a286 --- /dev/null +++ b/homeassistant/components/rpi_camera/const.py @@ -0,0 +1,22 @@ +"""Consts used by rpi_camera.""" + +DOMAIN = "rpi_camera" + +CONF_HORIZONTAL_FLIP = "horizontal_flip" +CONF_IMAGE_HEIGHT = "image_height" +CONF_IMAGE_QUALITY = "image_quality" +CONF_IMAGE_ROTATION = "image_rotation" +CONF_IMAGE_WIDTH = "image_width" +CONF_OVERLAY_METADATA = "overlay_metadata" +CONF_OVERLAY_TIMESTAMP = "overlay_timestamp" +CONF_TIMELAPSE = "timelapse" +CONF_VERTICAL_FLIP = "vertical_flip" + +DEFAULT_HORIZONTAL_FLIP = 0 +DEFAULT_IMAGE_HEIGHT = 480 +DEFAULT_IMAGE_QUALITY = 7 +DEFAULT_IMAGE_ROTATION = 0 +DEFAULT_IMAGE_WIDTH = 640 +DEFAULT_NAME = "Raspberry Pi Camera" +DEFAULT_TIMELAPSE = 1000 +DEFAULT_VERTICAL_FLIP = 0 diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 3e38da47eed..a7ecaa3d36c 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import rpi_gpio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors, True) -class RPiGPIOBinarySensor(BinarySensorDevice): +class RPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses Raspberry Pi GPIO.""" def __init__(self, name, port, pull_mode, bouncetime, invert_logic): diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py index 648171b9738..56e76959ecc 100644 --- a/homeassistant/components/rpi_gpio/cover.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -5,7 +5,7 @@ from time import sleep import voluptuous as vol from homeassistant.components import rpi_gpio -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(covers) -class RPiGPIOCover(CoverDevice): +class RPiGPIOCover(CoverEntity): """Representation of a Raspberry GPIO cover.""" def __init__( diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py index 89d44a0e8db..e77ceea3eb7 100644 --- a/homeassistant/components/rpi_pfio/binary_sensor.py +++ b/homeassistant/components/rpi_pfio/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import rpi_pfio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): rpi_pfio.activate_listener(hass) -class RPiPFIOBinarySensor(BinarySensorDevice): +class RPiPFIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that a PiFace Digital Input.""" def __init__(self, hass, port, name, settle_time, invert_logic): diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py index 5c09111c1cb..78c2153a7b3 100644 --- a/homeassistant/components/rpi_rf/switch.py +++ b/homeassistant/components/rpi_rf/switch.py @@ -5,7 +5,7 @@ from threading import RLock 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, CONF_SWITCHES, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: rfdevice.cleanup()) -class RPiRFSwitch(SwitchDevice): +class RPiRFSwitch(SwitchEntity): """Representation of a GPIO RF switch.""" def __init__( diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index a1fe057d9fb..d65ef1ce0e5 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -4,7 +4,7 @@ import logging from russound_rio import Russound import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, @@ -73,7 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class RussoundZoneDevice(MediaPlayerDevice): +class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" def __init__(self, russ, zone_id, name, sources): diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index 70ed1212363..e1a6430c9cc 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -4,7 +4,7 @@ import logging from russound import russound import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Not connected to %s:%s", host, port) -class RussoundRNETDevice(MediaPlayerDevice): +class RussoundRNETDevice(MediaPlayerEntity): """Representation of a Russound RNET device.""" def __init__(self, hass, russ, sources, zone_id, extra): diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index c4002f252e8..fdd999ac684 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -2,6 +2,6 @@ "domain": "saj", "name": "SAJ Solar Inverter", "documentation": "https://www.home-assistant.io/integrations/saj", - "requirements": ["pysaj==0.0.14"], + "requirements": ["pysaj==0.0.16"], "codeowners": ["@fredericvl"] } diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index fb0df36d4cb..1e7b3dd5061 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -91,9 +91,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady for sensor in sensor_def: - hass_sensors.append( - SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) - ) + if sensor.enabled: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) async_add_entities(hass_sensors) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 35a374688b5..7eb0f50efc2 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -4,7 +4,7 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerDevice +from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -81,7 +81,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)]) -class SamsungTVDevice(MediaPlayerDevice): +class SamsungTVDevice(MediaPlayerEntity): """Representation of a Samsung TV.""" def __init__(self, bridge, config_entry, on_script): diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index 4377e0671e9..e615728776e 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -4,7 +4,10 @@ "step": { "user": { "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", - "data": { "host": "Host or IP address", "name": "Name" } + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "Name" + } }, "confirm": { "title": "Samsung TV", @@ -19,4 +22,4 @@ "not_supported": "This Samsung TV device is currently not supported." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 53a5cd4d061..7ee7c6bd8f4 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Host or IP address", + "host": "Host", "name": "Name" }, "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", diff --git a/homeassistant/components/samsungtv/translations/es-419.json b/homeassistant/components/samsungtv/translations/es-419.json new file mode 100644 index 00000000000..b35146e181e --- /dev/null +++ b/homeassistant/components/samsungtv/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Esta televisi\u00f3n Samsung ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n de la televisi\u00f3n Samsung ya est\u00e1 en progreso.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta televisi\u00f3n Samsung. Verifique la configuraci\u00f3n de su televisi\u00f3n para autorizar Home Assistant.", + "not_successful": "No se puede conectar a este dispositivo de TV Samsung.", + "not_supported": "Este dispositivo Samsung TV no es compatible actualmente." + }, + "flow_title": "Televisi\u00f3n Samsung: {model}", + "step": { + "confirm": { + "description": "\u00bfDesea configurar la televisi\u00f3n Samsung {model}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Ingrese la informaci\u00f3n de su televisor Samsung. Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n.", + "title": "Samsung TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index fc997b4974d..a12cce712ed 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -18,7 +18,7 @@ "host": "Host o direcci\u00f3n IP", "name": "Nombre" }, - "description": "Introduce la informaci\u00f3n de tu televisor Samsung. Si nunca conect\u00f3 Home Assistant antes de ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n.", + "description": "Introduce la informaci\u00f3n de tu televisi\u00f3n Samsung. Si nunca antes te conectaste con Home Assistant, deber\u00edas ver un mensaje en tu televisi\u00f3n pidiendo autorizaci\u00f3n.", "title": "Samsung TV" } } diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index f6f09dab4ee..1704fa04897 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -3,14 +3,14 @@ "abort": { "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.", "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.", - "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV-k\u00e9sz\u00fcl\u00e9k\u00e9ben a Home Assistant enged\u00e9lyez\u00e9si be\u00e1ll\u00edt\u00e1sait.", + "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", "not_successful": "Nem lehet csatlakozni ehhez a Samsung TV k\u00e9sz\u00fcl\u00e9khez.", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." }, "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Be\u00e1ll\u00edtja a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha soha nem csatlakozott home assistant-hez ezel\u0151tt, meg kell jelennie egy felugr\u00f3 ablaknak a TV-ben, ahol hiteles\u00edt\u00e9st k\u00e9r. A tv-k\u00e9sz\u00fcl\u00e9k manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r. A TV manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", "title": "Samsung TV" }, "user": { @@ -18,7 +18,7 @@ "host": "Hosztn\u00e9v vagy IP c\u00edm", "name": "N\u00e9v" }, - "description": "\u00cdrja be a Samsung TV adatait. Ha soha nem csatlakoztatta a Home Assistant alkalmaz\u00e1st ezel\u0151tt, l\u00e1tnia kell a t\u00e9v\u00e9ben egy felugr\u00f3 ablakot, amely enged\u00e9lyt k\u00e9r.", + "description": "\u00cdrd be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r.", "title": "Samsung TV" } } diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 228c8b1f71e..523d9f8c45e 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", "not_successful": "\uc0bc\uc131 TV \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" }, "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 9240842ff97..ac6b4ac87f8 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -3,11 +3,11 @@ "abort": { "already_configured": "Denne Samsung TV-en er allerede konfigurert.", "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.", - "auth_missing": "Home Assistant er ikke autorisert til \u00e5 koble til denne Samsung-TV. Vennligst kontroller innstillingene for TV-en for \u00e5 autorisere Home Assistent.", + "auth_missing": "Home Assistant er ikke godkjent til \u00e5 koble til denne Samsung-TV. Vennligst kontroller innstillingene for TV-en for \u00e5 godkjenne Home Assistent.", "not_successful": "Kan ikke koble til denne Samsung TV-enheten.", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, - "flow_title": "", + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", @@ -18,7 +18,7 @@ "host": "Vert eller IP-adresse", "name": "Navn" }, - "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", + "description": "Fyll inn Samsung TV-informasjonen din. Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", "title": "" } } diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 1124b5335a8..94a909680fa 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa" }, "description": "Wprowad\u017a informacje o telewizorze Samsung. Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant'em na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.", diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index ab1135498cf..53b9dcc3206 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index ac440b1225c..2442cbcaf5f 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, "description": "\u8f38\u5165\u4e09\u661f\u96fb\u8996\u8cc7\u8a0a\u3002\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 8a240794580..6f1532e1013 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -52,7 +52,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): +class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): """Representation of an AlarmDecoder-based alarm panel.""" def __init__(self, controller, name, arm_home_mode, partition_id): diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 4a9be339a1c..19763903f27 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Satel Integra zone states- represented as binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -49,7 +49,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class SatelIntegraBinarySensor(BinarySensorDevice): +class SatelIntegraBinarySensor(BinarySensorEntity): """Representation of an Satel Integra binary sensor.""" def __init__( diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 46f4de2784f..e747b2474ae 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -1,7 +1,7 @@ """Support for Satel Integra modifiable outputs represented as switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -39,7 +39,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class SatelIntegraSwitch(SwitchDevice): +class SatelIntegraSwitch(SwitchEntity): """Representation of an Satel switch.""" def __init__(self, controller, device_number, device_name, code): diff --git a/homeassistant/components/scene/translations/no.json b/homeassistant/components/scene/translations/no.json index 827c0c81f38..d8a4c453015 100644 --- a/homeassistant/components/scene/translations/no.json +++ b/homeassistant/components/scene/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Scene" + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index d91020c7c2b..88630cb99db 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -8,7 +8,7 @@ from homeassistant.components.climate import ( PLATFORM_SCHEMA, SCAN_INTERVAL, TEMP_CELSIUS, - ClimateDevice, + ClimateEntity, ) from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -63,7 +63,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class SchluterThermostat(ClimateDevice): +class SchluterThermostat(ClimateEntity): """Representation of a Schluter thermostat.""" def __init__(self, coordinator, serial_number, api, session_id): diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 13d99a0cb8f..4119f7e4c6b 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -132,7 +132,11 @@ class ScrapeSensor(Entity): if self._attr is not None: value = raw_data.select(self._select)[self._index][self._attr] else: - value = raw_data.select(self._select)[self._index].text + tag = raw_data.select(self._select)[self._index] + if tag.name in ("style", "script", "template"): + value = tag.string + else: + value = tag.text _LOGGER.debug(value) except IndexError: _LOGGER.error("Unable to extract data from HTML") diff --git a/homeassistant/components/script/translations/no.json b/homeassistant/components/script/translations/no.json index caeaf751b81..28122450085 100644 --- a/homeassistant/components/script/translations/no.json +++ b/homeassistant/components/script/translations/no.json @@ -1,3 +1,9 @@ { - "title": "Script" + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index f9ef2e12730..0c7d057316c 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -8,7 +8,7 @@ from scsgate.tasks import ( ) import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(covers) -class SCSGateCover(CoverDevice): +class SCSGateCover(CoverEntity): """Representation of SCSGate cover.""" def __init__(self, scs_id, name, logger, scsgate): diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index 8caa824d5fb..c8f5b720c70 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -4,7 +4,7 @@ import logging from scsgate.tasks import ToggleStatusTask import voluptuous as vol -from homeassistant.components.light import PLATFORM_SCHEMA, Light +from homeassistant.components.light import PLATFORM_SCHEMA, LightEntity from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): scsgate.add_devices_to_register(lights) -class SCSGateLight(Light): +class SCSGateLight(LightEntity): """Representation of a SCSGate light.""" def __init__(self, scs_id, name, logger, scsgate): diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index d5c85a5a84f..da69eb0b058 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -5,7 +5,7 @@ from scsgate.messages import ScenarioTriggeredMessage, StateMessage from scsgate.tasks import ToggleStatusTask import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -80,7 +80,7 @@ def _setup_scenario_switches(logger, config, scsgate, hass): scsgate.add_device(switch) -class SCSGateSwitch(SwitchDevice): +class SCSGateSwitch(SwitchEntity): """Representation of a SCSGate switch.""" def __init__(self, scs_id, name, logger, scsgate): diff --git a/homeassistant/components/season/translations/sensor.es-419.json b/homeassistant/components/season/translations/sensor.es-419.json index c0c3927357b..6837038ff3c 100644 --- a/homeassistant/components/season/translations/sensor.es-419.json +++ b/homeassistant/components/season/translations/sensor.es-419.json @@ -1,9 +1,16 @@ { "state": { + "season__season": { + "autumn": "Oto\u00f1o", + "spring": "Primavera", + "summer": "Verano", + "winter": "Invierno" + }, "season__season__": { "autumn": "Oto\u00f1o", "spring": "Primavera", - "summer": "Verano" + "summer": "Verano", + "winter": "Invierno" } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.fi.json b/homeassistant/components/season/translations/sensor.fi.json index f01f6451549..012735e07f8 100644 --- a/homeassistant/components/season/translations/sensor.fi.json +++ b/homeassistant/components/season/translations/sensor.fi.json @@ -1,8 +1,10 @@ { "state": { - "autumn": "Syksy", - "spring": "Kev\u00e4t", - "summer": "Kes\u00e4", - "winter": "Talvi" + "season__season": { + "autumn": "Syksy", + "spring": "Kev\u00e4t", + "summer": "Kes\u00e4", + "winter": "Talvi" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.hu.json b/homeassistant/components/season/translations/sensor.hu.json index 66a9972f3bd..ae0bdb2713c 100644 --- a/homeassistant/components/season/translations/sensor.hu.json +++ b/homeassistant/components/season/translations/sensor.hu.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "\u0150sz", + "spring": "Tavasz", + "summer": "Ny\u00e1r", + "winter": "T\u00e9l" + }, "season__season__": { "autumn": "\u0150sz", "spring": "Tavasz", diff --git a/homeassistant/components/season/translations/sensor.it.json b/homeassistant/components/season/translations/sensor.it.json index e584633325d..d66fb3f09af 100644 --- a/homeassistant/components/season/translations/sensor.it.json +++ b/homeassistant/components/season/translations/sensor.it.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Autunno", + "spring": "Primavera", + "summer": "Estate", + "winter": "Inverno" + }, "season__season__": { "autumn": "Autunno", "spring": "Primavera", diff --git a/homeassistant/components/season/translations/sensor.sl.json b/homeassistant/components/season/translations/sensor.sl.json index 6be24690d37..329d83bd1de 100644 --- a/homeassistant/components/season/translations/sensor.sl.json +++ b/homeassistant/components/season/translations/sensor.sl.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Jesen", + "spring": "Pomlad", + "summer": "Poletje", + "winter": "Zima" + }, "season__season__": { "autumn": "Jesen", "spring": "Pomlad", diff --git a/homeassistant/components/season/translations/sensor.sv.json b/homeassistant/components/season/translations/sensor.sv.json index 59875084928..bffbdaa8d2f 100644 --- a/homeassistant/components/season/translations/sensor.sv.json +++ b/homeassistant/components/season/translations/sensor.sv.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "H\u00f6st", + "spring": "V\u00e5r", + "summer": "Sommar", + "winter": "Vinter" + }, "season__season__": { "autumn": "H\u00f6st", "spring": "V\u00e5r", diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index 384bc3d074f..62fe0d01d4e 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -1,7 +1,7 @@ """Support for monitoring a Sense energy sensor device.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, DEVICE_CLASS_POWER from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -61,7 +61,7 @@ def sense_to_mdi(sense_icon): return "mdi:{}".format(MDI_ICONS.get(sense_icon, "power-plug")) -class SenseDevice(BinarySensorDevice): +class SenseDevice(BinarySensorEntity): """Implementation of a Sense energy device binary sensor.""" def __init__(self, sense_devices_data, device, sense_monitor_id): diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index d0bed826734..deadf759f06 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.7.1"], + "requirements": ["sense_energy==0.7.2"], "codeowners": ["@kbickar"], "config_flow": true } diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json index 6c90fdbaab8..1363d44d89f 100644 --- a/homeassistant/components/sense/strings.json +++ b/homeassistant/components/sense/strings.json @@ -3,7 +3,10 @@ "step": { "user": { "title": "Connect to your Sense Energy Monitor", - "data": { "email": "Email Address", "password": "Password" } + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -11,6 +14,8 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/en.json b/homeassistant/components/sense/translations/en.json index a5915ae2ced..181dadea06c 100644 --- a/homeassistant/components/sense/translations/en.json +++ b/homeassistant/components/sense/translations/en.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Email Address", + "email": "Email", "password": "Password" }, "title": "Connect to your Sense Energy Monitor" diff --git a/homeassistant/components/sense/translations/es-419.json b/homeassistant/components/sense/translations/es-419.json new file mode 100644 index 00000000000..7f643e13d63 --- /dev/null +++ b/homeassistant/components/sense/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "email": "Direcci\u00f3n de correo electr\u00f3nico", + "password": "Contrase\u00f1a" + }, + "title": "Con\u00e9ctese a su monitor de energ\u00eda Sense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index 11759880108..bf5509a44a8 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/sense/translations/ko.json b/homeassistant/components/sense/translations/ko.json index 154094ae971..26545db739a 100644 --- a/homeassistant/components/sense/translations/ko.json +++ b/homeassistant/components/sense/translations/ko.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "\uc774\uba54\uc77c \uc8fc\uc18c", + "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" }, "title": "Sense Energy Monitor \uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json new file mode 100644 index 00000000000..ee9e61b5a38 --- /dev/null +++ b/homeassistant/components/sense/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "E-mailadres", + "password": "Wachtwoord" + }, + "title": "Maak verbinding met uw Sense Energy Monitor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pl.json b/homeassistant/components/sense/translations/pl.json index 6c54984b7cd..3a1a75d1c14 100644 --- a/homeassistant/components/sense/translations/pl.json +++ b/homeassistant/components/sense/translations/pl.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "email": "Adres e-mail", - "password": "Has\u0142o" + "email": "[%key_id:common::config_flow::data::email%]", + "password": "[%key_id:common::config_flow::data::password%]" }, "title": "Po\u0142\u0105czenie z monitorem energii Sense" } diff --git a/homeassistant/components/sense/translations/sv.json b/homeassistant/components/sense/translations/sv.json new file mode 100644 index 00000000000..02939a27dbb --- /dev/null +++ b/homeassistant/components/sense/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-postadress", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json index 474634692e4..e9890263477 100644 --- a/homeassistant/components/sense/translations/zh-Hant.json +++ b/homeassistant/components/sense/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740", + "email": "\u96fb\u5b50\u90f5\u4ef6", "password": "\u5bc6\u78bc" }, "title": "\u9023\u7dda\u81f3 Sense \u80fd\u6e90\u76e3\u63a7" diff --git a/homeassistant/components/sensehat/light.py b/homeassistant/components/sensehat/light.py index 462c4245cd4..5eda783bebf 100644 --- a/homeassistant/components/sensehat/light.py +++ b/homeassistant/components/sensehat/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.const import 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([SenseHatLight(sensehat, name)]) -class SenseHatLight(Light): +class SenseHatLight(LightEntity): """Representation of an Sense Hat Light.""" def __init__(self, sensehat, name): diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 08e2212e2a2..93d992d1d89 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -8,7 +8,7 @@ import async_timeout import pysensibo import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -135,7 +135,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class SensiboClimate(ClimateDevice): +class SensiboClimate(ClimateEntity): """Representation of a Sensibo device.""" def __init__(self, client, data, units): diff --git a/homeassistant/components/sensor/translations/es-419.json b/homeassistant/components/sensor/translations/es-419.json index 6ed28293a90..e724fe3a106 100644 --- a/homeassistant/components/sensor/translations/es-419.json +++ b/homeassistant/components/sensor/translations/es-419.json @@ -1,4 +1,15 @@ { + "device_automation": { + "trigger_type": { + "battery_level": "{entity_name} cambios de nivel de bater\u00eda", + "humidity": "{entity_name} cambios de humedad", + "illuminance": "{entity_name} cambios de iluminancia", + "pressure": "{entity_name} cambios de presi\u00f3n", + "signal_strength": "{entity_name} cambios en la intensidad de la se\u00f1al", + "temperature": "{entity_name} cambios de temperatura", + "value": "{entity_name} cambios de valor" + } + }, "state": { "_": { "off": "", diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index e09aa8dead0..80b6822607a 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -23,5 +23,11 @@ "value": "{entity_name} verdi endringer" } }, - "title": "Sensor" + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/season.pl.json b/homeassistant/components/sensor/translations/season.pl.json index f5a7da57e7f..9b313e511c9 100644 --- a/homeassistant/components/sensor/translations/season.pl.json +++ b/homeassistant/components/sensor/translations/season.pl.json @@ -1,8 +1,8 @@ { "state": { - "autumn": "Jesie\u0144", - "spring": "Wiosna", - "summer": "Lato", - "winter": "Zima" + "autumn": "jesie\u0144", + "spring": "wiosna", + "summer": "lato", + "winter": "zima" } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/es-419.json b/homeassistant/components/sentry/translations/es-419.json new file mode 100644 index 00000000000..7d207a3d5f2 --- /dev/null +++ b/homeassistant/components/sentry/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ya est\u00e1 configurado" + }, + "error": { + "bad_dsn": "DSN inv\u00e1lido", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "description": "Ingrese su DSN Sentry", + "title": "Centinela" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json index e9bf022b12c..2e350dbfb0a 100644 --- a/homeassistant/components/sentry/translations/pl.json +++ b/homeassistant/components/sentry/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "bad_dsn": "Nieprawid\u0142owy DSN", - "unknown": "Niespodziewany b\u0142\u0105d" + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 905167ea3ff..e0bf23a2514 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -17,9 +17,21 @@ _LOGGER = logging.getLogger(__name__) CONF_SERIAL_PORT = "serial_port" CONF_BAUDRATE = "baudrate" +CONF_BYTESIZE = "bytesize" +CONF_PARITY = "parity" +CONF_STOPBITS = "stopbits" +CONF_XONXOFF = "xonxoff" +CONF_RTSCTS = "rtscts" +CONF_DSRDTR = "dsrdtr" DEFAULT_NAME = "Serial Sensor" DEFAULT_BAUDRATE = 9600 +DEFAULT_BYTESIZE = serial_asyncio.serial.EIGHTBITS +DEFAULT_PARITY = serial_asyncio.serial.PARITY_NONE +DEFAULT_STOPBITS = serial_asyncio.serial.STOPBITS_ONE +DEFAULT_XONXOFF = False +DEFAULT_RTSCTS = False +DEFAULT_DSRDTR = False PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -27,6 +39,33 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_BYTESIZE, default=DEFAULT_BYTESIZE): vol.In( + [ + serial_asyncio.serial.FIVEBITS, + serial_asyncio.serial.SIXBITS, + serial_asyncio.serial.SEVENBITS, + serial_asyncio.serial.EIGHTBITS, + ] + ), + vol.Optional(CONF_PARITY, default=DEFAULT_PARITY): vol.In( + [ + serial_asyncio.serial.PARITY_NONE, + serial_asyncio.serial.PARITY_EVEN, + serial_asyncio.serial.PARITY_ODD, + serial_asyncio.serial.PARITY_MARK, + serial_asyncio.serial.PARITY_SPACE, + ] + ), + vol.Optional(CONF_STOPBITS, default=DEFAULT_STOPBITS): vol.In( + [ + serial_asyncio.serial.STOPBITS_ONE, + serial_asyncio.serial.STOPBITS_ONE_POINT_FIVE, + serial_asyncio.serial.STOPBITS_TWO, + ] + ), + vol.Optional(CONF_XONXOFF, default=DEFAULT_XONXOFF): cv.boolean, + vol.Optional(CONF_RTSCTS, default=DEFAULT_RTSCTS): cv.boolean, + vol.Optional(CONF_DSRDTR, default=DEFAULT_DSRDTR): cv.boolean, } ) @@ -36,12 +75,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) port = config.get(CONF_SERIAL_PORT) baudrate = config.get(CONF_BAUDRATE) + bytesize = config.get(CONF_BYTESIZE) + parity = config.get(CONF_PARITY) + stopbits = config.get(CONF_STOPBITS) + xonxoff = config.get(CONF_XONXOFF) + rtscts = config.get(CONF_RTSCTS) + dsrdtr = config.get(CONF_DSRDTR) value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - sensor = SerialSensor(name, port, baudrate, value_template) + sensor = SerialSensor( + name, + port, + baudrate, + bytesize, + parity, + stopbits, + xonxoff, + rtscts, + dsrdtr, + value_template, + ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.stop_serial_read) async_add_entities([sensor], True) @@ -50,12 +106,30 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SerialSensor(Entity): """Representation of a Serial sensor.""" - def __init__(self, name, port, baudrate, value_template): + def __init__( + self, + name, + port, + baudrate, + bytesize, + parity, + stopbits, + xonxoff, + rtscts, + dsrdtr, + value_template, + ): """Initialize the Serial sensor.""" self._name = name self._state = None self._port = port self._baudrate = baudrate + self._bytesize = bytesize + self._parity = parity + self._stopbits = stopbits + self._xonxoff = xonxoff + self._rtscts = rtscts + self._dsrdtr = dsrdtr self._serial_loop_task = None self._template = value_template self._attributes = None @@ -63,17 +137,46 @@ class SerialSensor(Entity): async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._serial_loop_task = self.hass.loop.create_task( - self.serial_read(self._port, self._baudrate) + self.serial_read( + self._port, + self._baudrate, + self._bytesize, + self._parity, + self._stopbits, + self._xonxoff, + self._rtscts, + self._dsrdtr, + ) ) - async def serial_read(self, device, rate, **kwargs): + async def serial_read( + self, + device, + baudrate, + bytesize, + parity, + stopbits, + xonxoff, + rtscts, + dsrdtr, + **kwargs, + ): """Read the data from the port.""" logged_error = False while True: try: reader, _ = await serial_asyncio.open_serial_connection( - url=device, baudrate=rate, **kwargs + url=device, + baudrate=baudrate, + bytesize=bytesize, + parity=parity, + stopbits=stopbits, + xonxoff=xonxoff, + rtscts=rtscts, + dsrdtr=dsrdtr, + **kwargs, ) + except SerialException as exc: if not logged_error: _LOGGER.exception( diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index fa12ff7a1b2..a2d205de240 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -4,7 +4,7 @@ from typing import Callable import pysesame2 import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice +from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_API_KEY, @@ -32,7 +32,7 @@ def setup_platform( ) -class SesameDevice(LockDevice): +class SesameDevice(LockEntity): """Representation of a Sesame device.""" def __init__(self, sesame: object) -> None: diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index ef155676d1f..352b96b5f22 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,6 +2,6 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==7.1.1"], + "requirements": ["pillow==7.1.2"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 89a1a20e8e4..dc9fd8769d6 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -56,7 +56,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: # pylint: disable=no-member create_process = asyncio.subprocess.create_subprocess_shell( cmd, - loop=hass.loop, stdin=None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -69,7 +68,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: # pylint: disable=no-member create_process = asyncio.subprocess.create_subprocess_exec( *shlexed_cmd, - loop=hass.loop, stdin=None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, diff --git a/homeassistant/components/shopping_list/translations/es-419.json b/homeassistant/components/shopping_list/translations/es-419.json new file mode 100644 index 00000000000..f9f1d78ea83 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "La lista de compras ya est\u00e1 configurada." + }, + "step": { + "user": { + "description": "\u00bfQuieres configurar la lista de compras?", + "title": "Lista de la compra" + } + } + }, + "title": "Lista de la compra" +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/nl.json b/homeassistant/components/shopping_list/translations/nl.json new file mode 100644 index 00000000000..de6045dd81b --- /dev/null +++ b/homeassistant/components/shopping_list/translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "De Shopping List is al geconfigureerd." + }, + "step": { + "user": { + "description": "Wil je de Shopping List configureren?", + "title": "Shopping List" + } + } + }, + "title": "Shopping List" +} \ No newline at end of file diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 78f38f5316f..1ad7effdf0e 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,6 +2,6 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==7.1.1", "simplehound==0.3"], + "requirements": ["pillow==7.1.2", "simplehound==0.3"], "codeowners": ["@robmarkcole"] } diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index f1db6a8af30..dcbf41307c4 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -3,5 +3,5 @@ "name": "Signal Messenger", "documentation": "https://www.home-assistant.io/integrations/signal_messenger", "codeowners": ["@bbernhard"], - "requirements": ["pysignalclirestapi==0.2.4"] + "requirements": ["pysignalclirestapi==0.3.4"] } diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 963195e6f64..a726c822cb0 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -223,7 +223,9 @@ async def async_setup_entry(hass, config_entry): websession = aiohttp_client.async_get_clientsession(hass) try: - api = await API.login_via_token(config_entry.data[CONF_TOKEN], websession) + api = await API.login_via_token( + config_entry.data[CONF_TOKEN], session=websession + ) except InvalidCredentialsError: _LOGGER.error("Invalid credentials provided") return False @@ -270,6 +272,17 @@ async def async_setup_entry(hass, config_entry): return decorator + @verify_system_exists + @_verify_domain_control + async def clear_notifications(call): + """Clear all active notifications.""" + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.clear_notifications() + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + @verify_system_exists @_verify_domain_control async def remove_pin(call): @@ -311,6 +324,7 @@ async def async_setup_entry(hass, config_entry): return for service, method, schema in [ + ("clear_notifications", clear_notifications, None), ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), ( diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 867a1044856..7998de463f6 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -21,7 +21,7 @@ from simplipy.websocket import ( from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, FORMAT_TEXT, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -72,7 +72,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): +class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Representation of a SimpliSafe alarm.""" def __init__(self, simplisafe, system): diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 031d5496f9d..1225f6de818 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -57,7 +57,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: simplisafe = await API.login_via_credentials( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD], websession + user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=websession ) except SimplipyError: return await self._show_form(errors={"base": "invalid_credentials"}) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index fc98d67ccbf..78866ce9004 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -5,7 +5,7 @@ from simplipy.errors import SimplipyError from simplipy.lock import LockStates from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.core import callback from . import SimpliSafeEntity @@ -30,7 +30,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class SimpliSafeLock(SimpliSafeEntity, LockDevice): +class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Define a SimpliSafe lock.""" def __init__(self, simplisafe, system, lock): diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index c03f05fc0c1..6b271012c8e 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.0.7"], + "requirements": ["simplisafe-python==9.2.0"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 3d9d832c99a..0a097c9fda8 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,8 +2,12 @@ "config": { "step": { "user": { - "title": "Fill in your information", - "data": { "username": "Email Address", "password": "Password" } + "title": "Fill in your information.", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]", + "code": "Code (used in Home Assistant UI)" + } } }, "error": { @@ -18,8 +22,10 @@ "step": { "init": { "title": "Configure SimpliSafe", - "data": { "code": "Code (used in Home Assistant UI)" } + "data": { + "code": "Code (used in Home Assistant UI)" + } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 32ff418a7bd..82cedbbca9f 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Codi (utilitzat a la UI de Home Assistant)", "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, @@ -21,7 +22,7 @@ "step": { "init": { "data": { - "code": "Codi (per la UI de Home Assistant)" + "code": "Codi (utilitzat a la UI de Home Assistant)" }, "title": "Configuraci\u00f3 de SimpliSafe" } diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 2c5a0d0971c..42fc575f650 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", "password": "Passwort", "username": "E-Mail-Adresse" }, diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 1cbaeffe958..90867a0163f 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -10,10 +10,11 @@ "step": { "user": { "data": { + "code": "Code (used in Home Assistant UI)", "password": "Password", - "username": "Email Address" + "username": "Email" }, - "title": "Fill in your information" + "title": "Fill in your information." } } }, diff --git a/homeassistant/components/simplisafe/translations/es-419.json b/homeassistant/components/simplisafe/translations/es-419.json index 135e9f843e9..6273cfa671b 100644 --- a/homeassistant/components/simplisafe/translations/es-419.json +++ b/homeassistant/components/simplisafe/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso." + }, "error": { "identifier_exists": "Cuenta ya registrada", "invalid_credentials": "Credenciales no v\u00e1lidas" @@ -7,11 +10,22 @@ "step": { "user": { "data": { + "code": "C\u00f3digo (utilizado en la interfaz de usuario de Home Assistant)", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico" }, "title": "Completa tu informaci\u00f3n" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "C\u00f3digo (utilizado en la interfaz de usuario de Home Assistant)" + }, + "title": "Configurar SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 8ffd687b228..8dbf1248fd6 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -10,10 +10,11 @@ "step": { "user": { "data": { + "code": "C\u00f3digo (utilizado en el interfaz de usuario de Home Assistant)", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico" }, - "title": "Completa tus datos" + "title": "Introduce tu informaci\u00f3n" } } }, diff --git a/homeassistant/components/simplisafe/translations/fi.json b/homeassistant/components/simplisafe/translations/fi.json new file mode 100644 index 00000000000..765616cd839 --- /dev/null +++ b/homeassistant/components/simplisafe/translations/fi.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Tili on jo rekister\u00f6ity", + "invalid_credentials": "Virheelliset tunnistetiedot" + }, + "step": { + "user": { + "data": { + "password": "Salasana", + "username": "S\u00e4hk\u00f6postiosoite" + }, + "title": "T\u00e4yt\u00e4 tietosi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 4454c82c8f8..730b9b810d1 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", "username": "Adresse e-mail" }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index c30d967d012..c63894ceaf2 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -10,10 +10,11 @@ "step": { "user": { "data": { + "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)", "password": "Password", "username": "Indirizzo E-mail" }, - "title": "Inserisci i tuoi dati" + "title": "Inserisci le tue informazioni." } } }, diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json index 97da5ac4e8b..44d3f453d41 100644 --- a/homeassistant/components/simplisafe/translations/ko.json +++ b/homeassistant/components/simplisafe/translations/ko.json @@ -10,10 +10,11 @@ "step": { "user": { "data": { + "code": "\ucf54\ub4dc (Home Assistant UI \uc5d0\uc11c \uc0ac\uc6a9\ub428)", "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + "username": "\uc774\uba54\uc77c" }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" + "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } }, @@ -23,7 +24,7 @@ "data": { "code": "\ucf54\ub4dc (Home Assistant UI \uc5d0\uc11c \uc0ac\uc6a9\ub428)" }, - "title": "SimpliSafe \uad6c\uc131" + "title": "SimpliSafe \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/simplisafe/translations/lb.json b/homeassistant/components/simplisafe/translations/lb.json index 8e460289ef3..0f1962c529c 100644 --- a/homeassistant/components/simplisafe/translations/lb.json +++ b/homeassistant/components/simplisafe/translations/lb.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Code (benotzt am Home Assistant Benotzer Interface)", "password": "Passwuert", "username": "E-Mail Adress" }, diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 0aeb9cb1d95..ce3fcf2902f 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dit SimpliSafe-account is al in gebruik." + }, "error": { "identifier_exists": "Account bestaat al", "invalid_credentials": "Ongeldige gebruikersgegevens" @@ -7,11 +10,22 @@ "step": { "user": { "data": { + "code": "Code (gebruikt in Home Assistant)", "password": "Wachtwoord", "username": "E-mailadres" }, "title": "Vul uw gegevens in" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "Code (gebruikt in de Home Assistant UI)" + }, + "title": "Configureer SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 8fcc86ffb82..3a253f2e5a1 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -10,10 +10,11 @@ "step": { "user": { "data": { + "code": "Kode (brukt i Home Assistant brukergrensesnittet)", "password": "Passord", "username": "E-postadresse" }, - "title": "Fyll ut informasjonen din" + "title": "Fyll ut informasjonen din." } } }, @@ -21,7 +22,7 @@ "step": { "init": { "data": { - "code": "Kode (brukt i home assistant ui)" + "code": "Kode (brukt i Home Assistant brukergrensesnittet)" }, "title": "Konfigurer SimpliSafe" } diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 6bfc6ce6037..9746aab1ee4 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -10,8 +10,9 @@ "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Adres e-mail" + "code": "Kod (u\u017cywany w interfejsie Home Assistant'a)", + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::email%]" }, "title": "Wprowad\u017a dane" } diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 4f0b9bf4ee3..26665617b1d 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "\u041a\u043e\u0434 (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, diff --git a/homeassistant/components/simplisafe/translations/sl.json b/homeassistant/components/simplisafe/translations/sl.json index 5b10cd96808..92e7a83f5b4 100644 --- a/homeassistant/components/simplisafe/translations/sl.json +++ b/homeassistant/components/simplisafe/translations/sl.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Koda (uporablja se v uporabni\u0161kem vmesniku Home Assistant)", "password": "Geslo", "username": "E-po\u0161tni naslov" }, diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 975c863d95d..2c522045cab 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -10,10 +10,11 @@ "step": { "user": { "data": { + "code": "\u9a57\u8b49\u78bc\uff08\u4f7f\u7528\u65bc Home Assistant UI\uff09", "password": "\u5bc6\u78bc", - "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + "username": "\u96fb\u5b50\u90f5\u4ef6" }, - "title": "\u586b\u5beb\u8cc7\u8a0a" + "title": "\u586b\u5beb\u8cc7\u8a0a\u3002" } } }, diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index 271db41ac22..ce0c37174ef 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -3,7 +3,7 @@ import logging import aiohttp -from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity from homeassistant.const import CONF_HOST from homeassistant.exceptions import PlatformNotReady @@ -26,7 +26,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SisyphusLight(table_holder.name, table)], update_before_add=True) -class SisyphusLight(Light): +class SisyphusLight(LightEntity): """Representation of a Sisyphus table as a light.""" def __init__(self, name, table): diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index 103ec694d83..dbc350453b7 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -4,7 +4,7 @@ import logging import aiohttp from sisyphus_control import Track -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -56,7 +56,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SisyphusPlayer(table_holder.name, host, table)], True) -class SisyphusPlayer(MediaPlayerDevice): +class SisyphusPlayer(MediaPlayerEntity): """Representation of a Sisyphus table as a media player device.""" def __init__(self, name, host, table): diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index f0df663eba3..a5c6681eb2b 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class SkybellBinarySensor(SkybellDevice, BinarySensorDevice): +class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): """A binary sensor implementation for Skybell devices.""" def __init__(self, device, sensor_type): diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index c9aa622ad0b..273cdfd079c 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -6,7 +6,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) import homeassistant.util.color as color_util @@ -36,7 +36,7 @@ def _to_hass_level(level): return int((level * 255) / 100) -class SkybellLight(SkybellDevice, Light): +class SkybellLight(SkybellDevice, LightEntity): """A binary sensor implementation for Skybell devices.""" def __init__(self, device): diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index 03ea74a2340..97a1d2a4c00 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -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_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -41,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class SkybellSwitch(SkybellDevice, SwitchDevice): +class SkybellSwitch(SkybellDevice, SwitchEntity): """A switch implementation for Skybell devices.""" def __init__(self, device, switch_type): diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index 3ba39a38764..39ae3e7c658 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -1,5 +1,5 @@ """Support for SleepIQ sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import SleepIQSensor from .const import DOMAIN, IS_IN_BED, SENSOR_TYPES, SIDES @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class IsInBedBinarySensor(SleepIQSensor, BinarySensorDevice): +class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): """Implementation of a SleepIQ presence sensor.""" def __init__(self, sleepiq_data, bed_id, side): diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index f50226b9f01..470cf9e5a1f 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -8,7 +8,7 @@ from homeassistant.components.cover import ( STATE_CLOSED, STATE_CLOSING, STATE_OPENING, - CoverDevice, + CoverEntity, ) from homeassistant.const import ATTR_ID @@ -32,7 +32,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class SlideCover(CoverDevice): +class SlideCover(CoverEntity): """Representation of a Slide cover.""" def __init__(self, api, slide): diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index cb7a1e8a395..6f6481d65f9 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -1,7 +1,7 @@ """Support for interacting with Smappee Comport Plugs.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DATA_SMAPPEE @@ -34,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class SmappeeSwitch(SwitchDevice): +class SmappeeSwitch(SwitchEntity): """Representation of a Smappee Comport Plug.""" def __init__(self, smappee, name, location_id, switch_id): diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index af55f2de7f9..09b8a7435ee 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, - CoverDevice, + CoverEntity, ) from . import DATA_HUB, DOMAIN @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class SmartHabCover(CoverDevice): +class SmartHabCover(CoverEntity): """Representation a cover.""" def __init__(self, cover): diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index 469d89011b8..8b608cfbd4f 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -5,7 +5,7 @@ import logging import pysmarthab from requests.exceptions import Timeout -from homeassistant.components.light import Light +from homeassistant.components.light import LightEntity from . import DATA_HUB, DOMAIN @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class SmartHabLight(Light): +class SmartHabLight(LightEntity): """Representation of a SmartHab Light.""" def __init__(self, light): diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 78d2c73ca73..825cf149952 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -3,7 +3,7 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -50,7 +50,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: ] -class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice): +class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity): """Define a SmartThings Binary Sensor.""" def __init__(self, device, attribute): diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 83a1af981db..6ce872cdac7 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,11 +1,12 @@ """Support for climate devices through the SmartThings cloud API.""" import asyncio +from collections.abc import Iterable import logging -from typing import Iterable, Optional, Sequence +from typing import Optional, Sequence from pysmartthings import Attribute, Capability -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateDevice +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -144,7 +145,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: return None -class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): +class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): """Define a SmartThings climate entities.""" def __init__(self, device): @@ -323,7 +324,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): return UNIT_MAP.get(self._device.status.attributes[Attribute.temperature].unit) -class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): +class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Define a SmartThings Air Conditioner.""" def __init__(self, device): diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index a41d9d6b9f7..ddc52ec3f6c 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -16,7 +16,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, - CoverDevice, + CoverEntity, ) from homeassistant.const import ATTR_BATTERY_LEVEL @@ -61,7 +61,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: return None -class SmartThingsCover(SmartThingsEntity, CoverDevice): +class SmartThingsCover(SmartThingsEntity, CoverEntity): """Define a SmartThings cover.""" def __init__(self, device): diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 7978d85505d..1e4161abd0f 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - Light, + LightEntity, ) import homeassistant.util.color as color_util @@ -61,7 +61,7 @@ def convert_scale(value, value_scale, target_scale, round_digits=4): return round(value * target_scale / value_scale, round_digits) -class SmartThingsLight(SmartThingsEntity, Light): +class SmartThingsLight(SmartThingsEntity, LightEntity): """Define a SmartThings Light.""" def __init__(self, device): diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index d249cc5ac94..d6b615b47a7 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -3,7 +3,7 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -38,7 +38,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: return None -class SmartThingsLock(SmartThingsEntity, LockDevice): +class SmartThingsLock(SmartThingsEntity, LockEntity): """Define a SmartThings lock.""" async def async_lock(self, **kwargs): diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 7d02a04d2ff..918ee455c27 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -30,6 +30,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import HomeAssistantType from .const import ( @@ -111,7 +112,11 @@ def get_webhook_url(hass: HomeAssistantType) -> str: def _get_app_template(hass: HomeAssistantType): - endpoint = f"at {hass.config.api.base_url}" + try: + endpoint = f"at {get_url(hass, allow_cloud=False, prefer_external=True)}" + except NoURLAvailableError: + endpoint = "" + cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url is not None: endpoint = "via Nabu Casa" diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c8b938ebc6a..3b8e3b208a6 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -8,7 +8,9 @@ "pat": { "title": "Enter Personal Access Token", "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.", - "data": { "access_token": "Access Token" } + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + } }, "select_location": { "title": "Select Location", diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index eb9c9c90c4b..ff70648ddcf 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -3,7 +3,7 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -29,7 +29,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: return None -class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): +class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): """Define a SmartThings switch.""" async def async_turn_off(self, **kwargs) -> None: diff --git a/homeassistant/components/smartthings/translations/ca.json b/homeassistant/components/smartthings/translations/ca.json index 3950d5027e9..0baa8147efe 100644 --- a/homeassistant/components/smartthings/translations/ca.json +++ b/homeassistant/components/smartthings/translations/ca.json @@ -6,9 +6,9 @@ }, "error": { "app_setup_error": "No s'ha pogut configurar SmartApp. Siusplau, torna-ho a provar.", - "token_forbidden": "El testimoni d'autenticaci\u00f3 no t\u00e9 cont\u00e9 els apartats OAuth obligatoris.", - "token_invalid_format": "El testimoni d'autenticaci\u00f3 ha d'estar en format UID/GUID", - "token_unauthorized": "El testimoni d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no t\u00e9 autoritzaci\u00f3.", + "token_forbidden": "El token d'autenticaci\u00f3 no t\u00e9 cont\u00e9 els apartats OAuth obligatoris.", + "token_invalid_format": "El token d'autenticaci\u00f3 ha d'estar en format UID/GUID", + "token_unauthorized": "El token d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no est\u00e0 autoritzat.", "webhook_error": "SmartThings no ha pogut validar l'adre\u00e7a final configurada a `base_url`. Revisa els requisits del component." }, "step": { @@ -17,10 +17,10 @@ }, "pat": { "data": { - "access_token": "Testimoni d'acc\u00e9s" + "access_token": "[%key::common::config_flow::data::access_token%]" }, - "description": "Introdueix un [testimoni d'acc\u00e9s personal]({token_url}) de SmartThings creat a partir de les [instruccions]({component_url}). S\u2019utilitzar\u00e0 per crear la integraci\u00f3 de Home Assistant dins el teu compte de SmartThings.", - "title": "Introdueix el testimoni d'acc\u00e9s personal" + "description": "Introdueix un [token d'acc\u00e9s personal]({token_url}) d'SmartThings creat a partir de les [instruccions]({component_url}). S'utilitzar\u00e0 per crear la integraci\u00f3 de Home Assistant dins el teu compte de SmartThings.", + "title": "Introdueix el token d'acc\u00e9s personal" }, "select_location": { "data": { @@ -31,7 +31,7 @@ }, "user": { "description": "SmartThings es configurar\u00e0 per a enviar actualitzacions push a Home Assistant a:\n> {webhook_url}\n\nSi no \u00e9s correcte, actualitza la teva configuraci\u00f3, reinicia Home Assistant i torna-ho a provar.", - "title": "Introdueix el testimoni d'autenticaci\u00f3 personal" + "title": "Introdueix el token d'autenticaci\u00f3 personal" } } } diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index 3dc406200f5..3c8d096403c 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -17,7 +17,7 @@ }, "pat": { "data": { - "access_token": "Zugangstoken" + "access_token": "Zugriffs-Token" }, "description": "Bitte geben Sie ein SmartThings [Personal Access Token] ({token_url}) ein, das gem\u00e4\u00df den [Anweisungen] ({component_url}) erstellt wurde. Dies wird zur Erstellung der Home Assistant-Integration in Ihrem SmartThings-Konto verwendet.", "title": "Gib den pers\u00f6nlichen Zugangstoken an" diff --git a/homeassistant/components/smartthings/translations/es-419.json b/homeassistant/components/smartthings/translations/es-419.json index d5446773700..bc5c6972fe4 100644 --- a/homeassistant/components/smartthings/translations/es-419.json +++ b/homeassistant/components/smartthings/translations/es-419.json @@ -1,13 +1,33 @@ { "config": { + "abort": { + "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida: \n > {webhook_url} \n\nActualice su configuraci\u00f3n seg\u00fan las [instrucciones] ({component_url}), reinicie Home Assistant e intente nuevamente.", + "no_available_locations": "No hay ubicaciones SmartThings disponibles para configurar en Home Assistant." + }, "error": { "app_setup_error": "No se puede configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", "webhook_error": "SmartThings no pudo validar el endpoint configurado en `base_url`. Por favor, revise los requisitos de los componentes." }, "step": { + "authorize": { + "title": "Autorizar Home Assistant" + }, + "pat": { + "description": "Ingrese un SmartThings [Token de acceso personal] ({token_url}) que se ha creado seg\u00fan las [instrucciones] ({component_url}). Esto se usar\u00e1 para crear la integraci\u00f3n de Home Assistant dentro de su cuenta SmartThings.", + "title": "Ingresar token de acceso personal" + }, + "select_location": { + "data": { + "location_id": "Ubicaci\u00f3n" + }, + "description": "Seleccione la ubicaci\u00f3n de SmartThings que desea agregar a Home Assistant. Luego, abriremos una nueva ventana y le pediremos que inicie sesi\u00f3n y autorice la instalaci\u00f3n de la integraci\u00f3n de Home Assistant en la ubicaci\u00f3n seleccionada.", + "title": "Seleccionar ubicaci\u00f3n" + }, "user": { + "description": "SmartThings se configurar\u00e1 para enviar actualizaciones push a Home Assistant en: \n > {webhook_url} \n\nSi esto no es correcto, actualice su configuraci\u00f3n, reinicie Home Assistant e intente nuevamente.", "title": "Ingresar token de acceso personal" } } diff --git a/homeassistant/components/smartthings/translations/fr.json b/homeassistant/components/smartthings/translations/fr.json index 1fbd4abada4..9902495e6a1 100644 --- a/homeassistant/components/smartthings/translations/fr.json +++ b/homeassistant/components/smartthings/translations/fr.json @@ -8,15 +8,20 @@ "webhook_error": "SmartThings n'a pas pu valider le point de terminaison configur\u00e9 en \u00ab\u00a0base_url\u00a0\u00bb. Veuillez consulter les exigences du composant." }, "step": { + "authorize": { + "title": "Autoriser Home Assistant" + }, "pat": { "data": { "access_token": "Jeton d'acc\u00e8s" - } + }, + "title": "Entrer un jeton d'acc\u00e8s personnel" }, "select_location": { "data": { "location_id": "Emplacement" - } + }, + "title": "S\u00e9lectionnez l'emplacement" }, "user": { "description": "Veuillez entrer un [jeton d'acc\u00e8s personnel SmartThings] ( {token_url} ) cr\u00e9\u00e9 selon les [instructions] ( {component_url} ).", diff --git a/homeassistant/components/smartthings/translations/ko.json b/homeassistant/components/smartthings/translations/ko.json index 383b1a9412c..0ee28136651 100644 --- a/homeassistant/components/smartthings/translations/ko.json +++ b/homeassistant/components/smartthings/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant \uac00 SmartThings \uc5d0\uc11c \uc5c5\ub370\uc774\ud2b8\ub97c \uc218\uc2e0\ud558\ub3c4\ub85d \uc62c\ubc14\ub974\uac8c \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc6f9 \ud6c4\ud06c URL \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4:\n> {webhook_url} \n\n[\uc548\ub0b4]({component_url}) \ub97c \ucc38\uace0\ud558\uc5ec \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_webhook_url": "Home Assistant \uac00 SmartThings \uc5d0\uc11c \uc5c5\ub370\uc774\ud2b8\ub97c \uc218\uc2e0\ud558\ub3c4\ub85d \uc62c\ubc14\ub974\uac8c \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc6f9 \ud6c5 URL \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4:\n> {webhook_url} \n\n[\uc548\ub0b4]({component_url}) \ub97c \ucc38\uace0\ud558\uc5ec \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "no_available_locations": "Home Assistant \uc5d0\uc11c \uc124\uc815\ud560 \uc218 \uc788\ub294 SmartThings \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "error": { @@ -9,7 +9,7 @@ "token_forbidden": "\ud1a0\ud070\uc5d0 \ud544\uc694\ud55c OAuth \ubc94\uc704\ubaa9\ub85d\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "webhook_error": "SmartThings \ub294 `base_url` \uc5d0 \uc124\uc815\ub41c \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc758 \uc720\ud6a8\uc131\uc744 \uac80\uc0ac \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc758 \uc694\uad6c \uc0ac\ud56d\uc744 \uac80\ud1a0\ud574\uc8fc\uc138\uc694." + "webhook_error": "SmartThings \uac00 \uc6f9 \ud6c5 URL \uc744 \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137\uc5d0\uc11c \uc6f9 \ud6c5 URL \uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\ub294\uc9c0 \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "step": { "authorize": { @@ -20,7 +20,7 @@ "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" }, "description": "[\uc548\ub0b4]({component_url}) \uc5d0 \ub530\ub77c \uc0dd\uc131\ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url}) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. SmartThings \uacc4\uc815\uc5d0\uc11c Home Assistant \uc5f0\ub3d9\uc744 \ub9cc\ub4dc\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4.", - "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 \uc785\ub825" + "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 \uc785\ub825\ud558\uae30" }, "select_location": { "data": { @@ -30,8 +30,8 @@ "title": "\uc704\uce58 \uc120\ud0dd\ud558\uae30" }, "user": { - "description": "[\uc548\ub0b4]({component_url}) \uc5d0 \ub530\ub77c \uc0dd\uc131 \ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url}) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 \uc785\ub825" + "description": "SmartThings \ub294 \uc544\ub798\uc758 \uc6f9 \ud6c5 \uc8fc\uc18c\ub85c Home Assistant \uc5d0 \ud478\uc2dc \uc5c5\ub370\uc774\ud2b8\ub97c \ubcf4\ub0b4\ub3c4\ub85d \uad6c\uc131\ub429\ub2c8\ub2e4. \n > {webhook_url} \n\n\uc774 \uad6c\uc131\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc73c\uba74 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "title": "\ucf5c\ubc31 URL \ud655\uc778\ud558\uae30" } } } diff --git a/homeassistant/components/smartthings/translations/lb.json b/homeassistant/components/smartthings/translations/lb.json index af1e70eedd8..60652829121 100644 --- a/homeassistant/components/smartthings/translations/lb.json +++ b/homeassistant/components/smartthings/translations/lb.json @@ -17,7 +17,7 @@ }, "pat": { "data": { - "access_token": "Acc\u00e8ss Jeton" + "access_token": "Acc\u00e8s Jeton" }, "description": "G\u00ebff w.e.g. ee pers\u00e9inlechen SmartThings [Acc\u00e8s Jeton]({token_url}) un dee via [d'Instruktiounen] ({component_url}) erstallt gouf. D\u00ebse g\u00ebtte benotzt fir d'Integratioun vum SmartThings Kont am Home Assistant.", "title": "Pers\u00e9inlechen Acc\u00e8ss Jeton uginn" diff --git a/homeassistant/components/smartthings/translations/nl.json b/homeassistant/components/smartthings/translations/nl.json index b7e40798e83..d29b9b7fc35 100644 --- a/homeassistant/components/smartthings/translations/nl.json +++ b/homeassistant/components/smartthings/translations/nl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_webhook_url": "Home Assistant is niet correct geconfigureerd om updates van SmartThings te ontvangen. De webhook-URL is ongeldig: \n > {webhook_url} \n\n Werk uw configuratie bij volgens de [instructies] ( {component_url} ), start de Home Assistant opnieuw op en probeer het opnieuw.", + "no_available_locations": "Er zijn geen beschikbare SmartThings-locaties om in te stellen in Home Assistant." + }, "error": { "app_setup_error": "Instellen van SmartApp mislukt. Probeer het opnieuw.", "token_forbidden": "Het token heeft niet de vereiste OAuth-scopes.", @@ -8,10 +12,18 @@ "webhook_error": "SmartThings kon het in 'base_url` geconfigureerde endpoint niet goedkeuren. Lees de componentvereisten door." }, "step": { + "authorize": { + "title": "Machtig Home Assistant" + }, + "pat": { + "description": "Voer een SmartThings [Personal Access Token] ( {token_url} ) in dat is gemaakt volgens de [instructies] ( {component_url} ). Dit wordt gebruikt om de Home Assistant-integratie te cre\u00ebren binnen uw SmartThings-account.", + "title": "Persoonlijk toegangstoken invoeren" + }, "select_location": { "data": { "location_id": "Locatie" }, + "description": "Selecteer de SmartThings-locatie die u aan de Home Assistant wilt toevoegen. We zullen dan een nieuw venster openen en u vragen om in te loggen en de installatie van de Home Assistant-integratie op de geselecteerde locatie te autoriseren.", "title": "Locatie selecteren" }, "user": { diff --git a/homeassistant/components/smartthings/translations/no.json b/homeassistant/components/smartthings/translations/no.json index 7f843bfd7da..c6945783c11 100644 --- a/homeassistant/components/smartthings/translations/no.json +++ b/homeassistant/components/smartthings/translations/no.json @@ -1,32 +1,32 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant er ikke konfigurert riktig for \u00e5 motta oppdateringer fra SmartThings. URLen til nettkroken er ugyldig: \n > {webhook_url} \n\n Oppdater konfigurasjonen i henhold til [instruksjonene] ( {component_url} ), start Home Assistant p\u00e5 nytt, og pr\u00f8v igjen.", + "invalid_webhook_url": "Home Assistant er ikke konfigurert riktig for \u00e5 motta oppdateringer fra SmartThings. URLen til nettkroken er ugyldig: \n > {webhook_url} \n\nVennligst oppdater konfigurasjonen i henhold til [instruksjonene]({component_url}), start Home Assistant p\u00e5 nytt, og pr\u00f8v igjen.", "no_available_locations": "Det er ingen tilgjengelige SmartThings-lokasjoner \u00e5 konfigurere i Home Assistant." }, "error": { "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene.", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", - "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert.", + "token_unauthorized": "Tokenet er ugyldig eller er ikke lenger godkjent.", "webhook_error": "SmartThings kan ikke validere URL-adressen for webhook. Kontroller at URL-adressen for webhook kan n\u00e5s fra Internett, og pr\u00f8v p\u00e5 nytt." }, "step": { "authorize": { - "title": "Autoriser Home Assistant" + "title": "Godkjenn Home Assistant" }, "pat": { "data": { "access_token": "Tilgangstoken" }, - "description": "Skriv inn et SmartThings [Personal Access Token] ( {token_url} ) som er opprettet i henhold til [instruksjonene] ( {component_url} ). Dette vil bli brukt til \u00e5 opprette Home Assistant-integrasjonen i SmartThings-kontoen din.", - "title": "Oppgi Personlig Tilgangstoken" + "description": "Vennligst fyll inn en SmartThings [personlig tilgangstoken]({token_url}) som er opprettet etter [instruksjonene]({component_url}).", + "title": "Fyll inn personlig tilgangstoken" }, "select_location": { "data": { "location_id": "Lokasjon" }, - "description": "Velg SmartThings Lokasjon du vil legge til Home Assistant. Vi \u00e5pner deretter et nytt vindu og ber deg om \u00e5 logge inn og autorisere installasjon av Home Assistant-integrasjonen p\u00e5 det valgte stedet.", + "description": "Vennligst velg SmartThings lokasjon du vil legge til Home Assistant. Vi \u00e5pner deretter et nytt vindu og ber deg om \u00e5 logge inn og godkjenne installasjon av Home Assistant-integrasjonen p\u00e5 det valgte stedet.", "title": "Velg Posisjon" }, "user": { diff --git a/homeassistant/components/smartthings/translations/pl.json b/homeassistant/components/smartthings/translations/pl.json index 517127a5091..b056a3297fe 100644 --- a/homeassistant/components/smartthings/translations/pl.json +++ b/homeassistant/components/smartthings/translations/pl.json @@ -17,14 +17,16 @@ }, "pat": { "data": { - "access_token": "Token dost\u0119pu" + "access_token": "[%key_id:common::config_flow::data::access_token%]" }, + "description": "Wprowad\u017a [token dost\u0119pu osobistego]({token_url}) SmartThings, kt\u00f3ry zosta\u0142 utworzony zgodnie z [instrukcj\u0105]({component_url}). Umo\u017cliwi to stworzenie integracji Home Assistant w ramach Twojego konta SmartThings.", "title": "Wprowad\u017a osobisty token dost\u0119pu" }, "select_location": { "data": { "location_id": "Lokalizacja" }, + "description": "Wybierz lokalizacj\u0119 SmartThings, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistant'a. Nast\u0119pnie otwarte zostanie nowe okno i zostaniesz poproszony o zalogowanie si\u0119 i autoryzacj\u0119 instalacji integracji Home Assistant w wybranej lokalizacji.", "title": "Wybierz lokalizacj\u0119" }, "user": { diff --git a/homeassistant/components/smartthings/translations/pt.json b/homeassistant/components/smartthings/translations/pt.json index 9f2ed5a4b90..efab29fd698 100644 --- a/homeassistant/components/smartthings/translations/pt.json +++ b/homeassistant/components/smartthings/translations/pt.json @@ -15,9 +15,6 @@ "title": "Autorizar o Home Assistant" }, "pat": { - "data": { - "access_token": "Token de Acesso" - }, "title": "Insira o Token de acesso pessoal" }, "select_location": { diff --git a/homeassistant/components/smartthings/translations/sl.json b/homeassistant/components/smartthings/translations/sl.json index b2959577adc..09af2bad739 100644 --- a/homeassistant/components/smartthings/translations/sl.json +++ b/homeassistant/components/smartthings/translations/sl.json @@ -17,7 +17,7 @@ }, "pat": { "data": { - "access_token": "\u017deton za dostop" + "access_token": "Dostopni \u017eeton" }, "description": "Vnesite SmartThings [\u017deton osebnega dostopa] ({token_url}), ki je bil ustvarjen po [navodilih] ({component_url}). To bo uporabljeno za ustvarjanje integracije Home Assistant v va\u0161em ra\u010dunu SmartThings.", "title": "Vnesite \u017eeton za osebni dostop" diff --git a/homeassistant/components/smartthings/translations/sv.json b/homeassistant/components/smartthings/translations/sv.json index a02483baa47..413a4279cdd 100644 --- a/homeassistant/components/smartthings/translations/sv.json +++ b/homeassistant/components/smartthings/translations/sv.json @@ -8,6 +8,15 @@ "webhook_error": "SmartThings kunde inte validera endpoint konfigurerad i \" base_url`. V\u00e4nligen granska kraven f\u00f6r komponenten." }, "step": { + "authorize": { + "title": "Auktorisera Home Assistant" + }, + "select_location": { + "data": { + "location_id": "Position" + }, + "title": "V\u00e4lj plats" + }, "user": { "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", "title": "Ange personlig \u00e5tkomsttoken" diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index a86b3548e95..f8b9114ae0e 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -2,7 +2,7 @@ import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -25,7 +25,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, True) -class SmartyBinarySensor(BinarySensorDevice): +class SmartyBinarySensor(BinarySensorEntity): """Representation of a Smarty Binary Sensor.""" def __init__(self, name, device_class, smarty): diff --git a/homeassistant/components/smhi/translations/fi.json b/homeassistant/components/smhi/translations/fi.json new file mode 100644 index 00000000000..c11e9dbdf70 --- /dev/null +++ b/homeassistant/components/smhi/translations/fi.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Leveysaste", + "longitude": "Pituusaste", + "name": "Nimi" + }, + "title": "Sijainti Ruotsissa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 0c5450b5ddd..f09491bf611 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -99,15 +99,6 @@ class SmhiWeather(WeatherEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" - - def fail(): - """Postpone updates.""" - self._fail_count += 1 - if self._fail_count < 3: - self.hass.helpers.event.async_call_later( - RETRY_TIMEOUT, self.retry_update() - ) - try: with async_timeout.timeout(10): self._forecasts = await self.get_weather_forecast() @@ -115,11 +106,15 @@ class SmhiWeather(WeatherEntity): except (asyncio.TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") - fail() + self._fail_count += 1 + if self._fail_count < 3: + self.hass.helpers.event.async_call_later( + RETRY_TIMEOUT, self.retry_update + ) - async def retry_update(self): + async def retry_update(self, _): """Retry refresh weather forecast.""" - self.async_update() + await self.async_update() async def get_weather_forecast(self) -> []: """Return the current forecasts from SMHI API.""" diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 1bd44959ab1..ab4b2415034 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -6,7 +6,7 @@ import snapcast.control from snapcast.control.server import CONTROL_PORT import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, @@ -110,7 +110,7 @@ async def handle_set_latency(entity, service_call): await entity.async_set_latency(service_call.data[ATTR_LATENCY]) -class SnapcastGroupDevice(MediaPlayerDevice): +class SnapcastGroupDevice(MediaPlayerEntity): """Representation of a Snapcast group device.""" def __init__(self, group, uid_part): @@ -200,7 +200,7 @@ class SnapcastGroupDevice(MediaPlayerDevice): await self._group.restore() -class SnapcastClientDevice(MediaPlayerDevice): +class SnapcastClientDevice(MediaPlayerEntity): """Representation of a Snapcast client device.""" def __init__(self, client, uid_part): diff --git a/homeassistant/components/snmp/const.py b/homeassistant/components/snmp/const.py index 90e445da554..b3a93cfe98b 100644 --- a/homeassistant/components/snmp/const.py +++ b/homeassistant/components/snmp/const.py @@ -8,6 +8,7 @@ CONF_DEFAULT_VALUE = "default_value" CONF_PRIV_KEY = "priv_key" CONF_PRIV_PROTOCOL = "priv_protocol" CONF_VERSION = "version" +CONF_VARTYPE = "vartype" DEFAULT_AUTH_PROTOCOL = "none" DEFAULT_COMMUNITY = "public" @@ -16,6 +17,7 @@ DEFAULT_NAME = "SNMP" DEFAULT_PORT = "161" DEFAULT_PRIV_PROTOCOL = "none" DEFAULT_VERSION = "1" +DEFAULT_VARTYPE = "none" SNMP_VERSIONS = {"1": 0, "2c": 1, "3": None} diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 578b97c801e..7210c8e5fd3 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,7 +1,6 @@ """Support for SNMP enabled switch.""" import logging -from pyasn1.type.univ import Integer import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, @@ -14,9 +13,23 @@ from pysnmp.hlapi.asyncio import ( getCmd, setCmd, ) +from pysnmp.proto.rfc1902 import ( + Counter32, + Counter64, + Gauge32, + Integer, + Integer32, + IpAddress, + Null, + ObjectIdentifier, + OctetString, + Opaque, + TimeTicks, + Unsigned32, +) 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_HOST, CONF_NAME, @@ -34,12 +47,14 @@ from .const import ( CONF_COMMUNITY, CONF_PRIV_KEY, CONF_PRIV_PROTOCOL, + CONF_VARTYPE, CONF_VERSION, DEFAULT_AUTH_PROTOCOL, DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_PRIV_PROTOCOL, + DEFAULT_VARTYPE, DEFAULT_VERSION, MAP_AUTH_PROTOCOLS, MAP_PRIV_PROTOCOLS, @@ -56,6 +71,22 @@ DEFAULT_COMMUNITY = "private" DEFAULT_PAYLOAD_OFF = 0 DEFAULT_PAYLOAD_ON = 1 +MAP_SNMP_VARTYPES = { + "Counter32": Counter32, + "Counter64": Counter64, + "Gauge32": Gauge32, + "Integer32": Integer32, + "Integer": Integer, + "IpAddress": IpAddress, + "Null": Null, + # some work todo to support tuple ObjectIdentifier, this just supports str + "ObjectIdentifier": ObjectIdentifier, + "OctetString": OctetString, + "Opaque": Opaque, + "TimeTicks": TimeTicks, + "Unsigned32": Unsigned32, +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, @@ -78,6 +109,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PRIV_PROTOCOL, default=DEFAULT_PRIV_PROTOCOL): vol.In( MAP_PRIV_PROTOCOLS ), + vol.Optional(CONF_VARTYPE, default=DEFAULT_VARTYPE): cv.string, } ) @@ -100,6 +132,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= privproto = config.get(CONF_PRIV_PROTOCOL) payload_on = config.get(CONF_PAYLOAD_ON) payload_off = config.get(CONF_PAYLOAD_OFF) + vartype = config.get(CONF_VARTYPE) async_add_entities( [ @@ -120,13 +153,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= payload_off, command_payload_on, command_payload_off, + vartype, ) ], True, ) -class SnmpSwitch(SwitchDevice): +class SnmpSwitch(SwitchEntity): """Representation of a SNMP switch.""" def __init__( @@ -147,11 +181,13 @@ class SnmpSwitch(SwitchDevice): payload_off, command_payload_on, command_payload_off, + vartype, ): """Initialize the switch.""" self._name = name self._baseoid = baseoid + self._vartype = vartype # Set the command OID to the base OID if command OID is unset self._commandoid = commandoid or baseoid @@ -191,17 +227,24 @@ class SnmpSwitch(SwitchDevice): async def async_turn_on(self, **kwargs): """Turn on the switch.""" - if self._command_payload_on.isdigit(): - await self._set(Integer(self._command_payload_on)) - else: - await self._set(self._command_payload_on) + # If vartype set, use it - http://snmplabs.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType + await self._execute_command(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - if self._command_payload_on.isdigit(): - await self._set(Integer(self._command_payload_off)) + await self._execute_command(self._command_payload_off) + + async def _execute_command(self, command): + # User did not set vartype and command is not a digit + if self._vartype == "none" and not self._command_payload_on.isdigit(): + await self._set(command) + # User set vartype Null, command must be an empty string + elif self._vartype == "Null": + await self._set(Null)("") + # user did not set vartype but command is digit: defaulting to Integer + # or user did set vartype else: - await self._set(self._command_payload_off) + await self._set(MAP_SNMP_VARTYPES.get(self._vartype, Integer)(command)) async def async_update(self): """Update the state.""" @@ -241,7 +284,6 @@ class SnmpSwitch(SwitchDevice): return self._state async def _set(self, value): - await setCmd( *self._request_args, ObjectType(ObjectIdentity(self._commandoid), value) ) diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json index 03b14c51a18..eb4c5cda1fd 100644 --- a/homeassistant/components/solaredge/strings.json +++ b/homeassistant/components/solaredge/strings.json @@ -6,11 +6,15 @@ "data": { "name": "The name of this installation", "site_id": "The SolarEdge site-id", - "api_key": "The API key for this site" + "api_key": "[%key:common::config_flow::data::api_key%]" } } }, - "error": { "site_exists": "This site_id is already configured" }, - "abort": { "site_exists": "This site_id is already configured" } + "error": { + "site_exists": "This site_id is already configured" + }, + "abort": { + "site_exists": "This site_id is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/ca.json b/homeassistant/components/solaredge/translations/ca.json index 56e1633e3a1..ca5d472c9d6 100644 --- a/homeassistant/components/solaredge/translations/ca.json +++ b/homeassistant/components/solaredge/translations/ca.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "api_key": "Clau API d\u2019aquest lloc", - "name": "Nom d\u2019aquesta instal\u00b7laci\u00f3", + "api_key": "Clau API d'aquest lloc", + "name": "Nom d'aquesta instal\u00b7laci\u00f3", "site_id": "SolarEdge site_id" }, "title": "Configuraci\u00f3 dels par\u00e0metres de l'API per aquesta instal\u00b7laci\u00f3" diff --git a/homeassistant/components/solaredge/translations/en.json b/homeassistant/components/solaredge/translations/en.json index 75ce6f53612..d3d1fb4e862 100644 --- a/homeassistant/components/solaredge/translations/en.json +++ b/homeassistant/components/solaredge/translations/en.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "The API key for this site", + "api_key": "API Key", "name": "The name of this installation", "site_id": "The SolarEdge site-id" }, diff --git a/homeassistant/components/solaredge/translations/es-419.json b/homeassistant/components/solaredge/translations/es-419.json new file mode 100644 index 00000000000..7cbd7f1b5a1 --- /dev/null +++ b/homeassistant/components/solaredge/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "error": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "api_key": "La clave API para este sitio", + "name": "El nombre de esta instalaci\u00f3n.", + "site_id": "La identificaci\u00f3n del sitio de SolarEdge" + }, + "title": "Definir los par\u00e1metros de API para esta instalaci\u00f3n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/ko.json b/homeassistant/components/solaredge/translations/ko.json index be49825c8fb..eb2d8c42a14 100644 --- a/homeassistant/components/solaredge/translations/ko.json +++ b/homeassistant/components/solaredge/translations/ko.json @@ -9,11 +9,11 @@ "step": { "user": { "data": { - "api_key": "\uc774 \uc0ac\uc774\ud2b8\uc758 API \ud0a4", + "api_key": "API \ud0a4", "name": "\uc774 \uc124\uce58\uc758 \uc774\ub984", "site_id": "SolarEdge site-id" }, - "title": "\uc774 \uc124\uce58\uc5d0 \ub300\ud55c API \ub9e4\uac1c\ubcc0\uc218\ub97c \uc815\uc758\ud574\uc8fc\uc138\uc694" + "title": "\uc774 \uc124\uce58\uc5d0 \ub300\ud55c API \ub9e4\uac1c\ubcc0\uc218\ub97c \uc815\uc758\ud558\uae30" } } } diff --git a/homeassistant/components/solaredge/translations/pl.json b/homeassistant/components/solaredge/translations/pl.json index afef3401cad..e304337d3e2 100644 --- a/homeassistant/components/solaredge/translations/pl.json +++ b/homeassistant/components/solaredge/translations/pl.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Klucz API dla tej strony", + "api_key": "[%key_id:common::config_flow::data::api_key%] dla tej strony", "name": "Nazwa tej instalacji", "site_id": "SolarEdge site-id" }, diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index a63485cf300..ab134fff57d 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "API \u91d1\u9470", + "api_key": "API \u5bc6\u9470", "name": "\u5b89\u88dd\u540d\u7a31", "site_id": "SolarEdge site-id" }, diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index 6d54e5a8be9..1a196315a30 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -4,7 +4,7 @@ "user": { "title": "Define your Solar-Log connection", "data": { - "host": "The hostname or ip-address of your Solar-Log device", + "host": "[%key:common::config_flow::data::host%]", "name": "The prefix to be used for your Solar-Log sensors" } } @@ -13,6 +13,8 @@ "already_configured": "Device is already configured", "cannot_connect": "Failed to connect, please verify host address" }, - "abort": { "already_configured": "Device is already configured" } + "abort": { + "already_configured": "Device is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/en.json b/homeassistant/components/solarlog/translations/en.json index 72c1acf7bd1..22dd35574ad 100644 --- a/homeassistant/components/solarlog/translations/en.json +++ b/homeassistant/components/solarlog/translations/en.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "The hostname or ip-address of your Solar-Log device", + "host": "Host", "name": "The prefix to be used for your Solar-Log sensors" }, "title": "Define your Solar-Log connection" diff --git a/homeassistant/components/solarlog/translations/es-419.json b/homeassistant/components/solarlog/translations/es-419.json new file mode 100644 index 00000000000..9f17072f424 --- /dev/null +++ b/homeassistant/components/solarlog/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar, verifique la direcci\u00f3n del host" + }, + "step": { + "user": { + "data": { + "host": "El nombre de host o la direcci\u00f3n IP de su dispositivo Solar-Log", + "name": "El prefijo que se utilizar\u00e1 para sus sensores de registro solar" + }, + "title": "Define tu conexi\u00f3n Solar-Log" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/ko.json b/homeassistant/components/solarlog/translations/ko.json index 058f9ac9a9c..66c6a2177d4 100644 --- a/homeassistant/components/solarlog/translations/ko.json +++ b/homeassistant/components/solarlog/translations/ko.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "host": "Solar-Log \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "host": "\ud638\uc2a4\ud2b8", "name": "Solar-Log \uc13c\uc11c\uc5d0 \uc0ac\uc6a9\ub420 \uc811\ub450\uc0ac" }, - "title": "Solar-Log \uc5f0\uacb0 \uc815\uc758" + "title": "Solar-Log \uc5f0\uacb0 \uc815\uc758\ud558\uae30" } } } diff --git a/homeassistant/components/solarlog/translations/pl.json b/homeassistant/components/solarlog/translations/pl.json index a61257b6d16..5add902c4fa 100644 --- a/homeassistant/components/solarlog/translations/pl.json +++ b/homeassistant/components/solarlog/translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" }, "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP urz\u0105dzenia Solar-Log", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Prefiks dla sensor\u00f3w Solar-Log" }, "title": "Zdefiniuj po\u0142\u0105czenie z Solar-Log" diff --git a/homeassistant/components/solarlog/translations/ru.json b/homeassistant/components/solarlog/translations/ru.json index ecaaee01aef..cf4adc2d623 100644 --- a/homeassistant/components/solarlog/translations/ru.json +++ b/homeassistant/components/solarlog/translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "host": "\u0425\u043e\u0441\u0442", "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 Solar-Log" }, "title": "Solar-Log" diff --git a/homeassistant/components/solarlog/translations/zh-Hant.json b/homeassistant/components/solarlog/translations/zh-Hant.json index 435b7564bcc..85ea05369c5 100644 --- a/homeassistant/components/solarlog/translations/zh-Hant.json +++ b/homeassistant/components/solarlog/translations/zh-Hant.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Solar-Log \u8a2d\u5099\u4e4b\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "host": "\u4e3b\u6a5f\u7aef", "name": "Solar-Log \u50b3\u611f\u5668\u6240\u4f7f\u7528\u5b57\u9996" }, "title": "\u5b9a\u7fa9 Solar-Log \u9023\u7dda" diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 9bfe903e724..f2929dd8ddd 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -2,7 +2,7 @@ import logging -from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class SomaCover(SomaEntity, CoverDevice): +class SomaCover(SomaEntity, CoverEntity): """Representation of a Soma cover device.""" def close_cover(self, **kwargs): diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index 7b82a658a72..8ab7335dd5c 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -7,13 +7,18 @@ "result_error": "SOMA Connect responded with error status.", "connection_error": "Failed to connect to SOMA Connect." }, - "create_entry": { "default": "Successfully authenticated with Soma." }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, "step": { "user": { - "data": { "host": "Host", "port": "Port" }, + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, "description": "Please enter connection settings of your SOMA Connect.", "title": "SOMA Connect" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/es-419.json b/homeassistant/components/soma/translations/es-419.json new file mode 100644 index 00000000000..11c50fd5fb9 --- /dev/null +++ b/homeassistant/components/soma/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "connection_error": "No se pudo conectar a SOMA Connect.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con estado de error." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Ingrese la configuraci\u00f3n de conexi\u00f3n de su SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index e71dd3c4ea5..4b9fe3b564d 100644 --- a/homeassistant/components/soma/translations/no.json +++ b/homeassistant/components/soma/translations/no.json @@ -2,21 +2,21 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", "connection_error": "Kunne ikke koble til SOMA Connect.", "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "result_error": "SOMA Connect svarte med feilstatus." }, "create_entry": { - "default": "Vellykket autentisering med Somfy." + "default": "Vellykket godkjenning med Somfy." }, "step": { "user": { "data": { "host": "Vert", - "port": "" + "port": "Port" }, - "description": "Vennligst skriv tilkoblingsinnstillingene for din SOMA Connect.", + "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect.", "title": "" } } diff --git a/homeassistant/components/soma/translations/pl.json b/homeassistant/components/soma/translations/pl.json index dc71b001bde..75f379de3d6 100644 --- a/homeassistant/components/soma/translations/pl.json +++ b/homeassistant/components/soma/translations/pl.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z SOMA Connect.", - "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]", "result_error": "SOMA Connect odpowiedzia\u0142 statusem b\u0142\u0119du." }, "create_entry": { @@ -13,8 +13,8 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port" + "host": "[%key_id:common::config_flow::data::host%]", + "port": "[%key_id:common::config_flow::data::port%]" }, "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia SOMA Connect.", "title": "SOMA Connect" diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index d0e555ed55c..cddde87d8f6 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -5,7 +5,7 @@ from pymfy.api.devices.category import Category from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - CoverDevice, + CoverEntity, ) from . import API, CONF_OPTIMISTIC, DEVICES, DOMAIN, SomfyEntity @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(await hass.async_add_executor_job(get_covers), True) -class SomfyCover(SomfyEntity, CoverDevice): +class SomfyCover(SomfyEntity, CoverEntity): """Representation of a Somfy cover device.""" def __init__(self, device, api, optimistic): diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index bc31d68ec1d..e96c91ecaea 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -2,7 +2,7 @@ from pymfy.api.devices.camera_protect import CameraProtect from pymfy.api.devices.category import Category -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import API, DEVICES, DOMAIN, SomfyEntity @@ -23,7 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(await hass.async_add_executor_job(get_shutters), True) -class SomfyCameraShutter(SomfyEntity, SwitchDevice): +class SomfyCameraShutter(SomfyEntity, SwitchEntity): """Representation of a Somfy Camera Shutter device.""" def __init__(self, device, api): diff --git a/homeassistant/components/somfy/translations/es-419.json b/homeassistant/components/somfy/translations/es-419.json index ea066156c71..3667a72315b 100644 --- a/homeassistant/components/somfy/translations/es-419.json +++ b/homeassistant/components/somfy/translations/es-419.json @@ -1,3 +1,17 @@ { - "title": "Somfy" + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta Somfy.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Somfy." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json index 64bd20acf94..6f8e3c3b993 100644 --- a/homeassistant/components/somfy/translations/no.json +++ b/homeassistant/components/somfy/translations/no.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Du kan kun konfigurere \u00e9n Somfy-konto.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", "missing_configuration": "Somfy-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, "create_entry": { - "default": "Vellykket autentisering med Somfy." + "default": "Vellykket godkjenning med Somfy." }, "step": { "pick_implementation": { - "title": "Velg autentiseringsmetode" + "title": "Velg godkjenningsmetode" } } } diff --git a/homeassistant/components/somfy/translations/pl.json b/homeassistant/components/somfy/translations/pl.json index 0f062f65faa..99d00914410 100644 --- a/homeassistant/components/somfy/translations/pl.json +++ b/homeassistant/components/somfy/translations/pl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Somfy.", - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent Somfy nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Somfy" }, "step": { "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelniania" + "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" } } } diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json index cc1fe4c17d9..85292205b28 100644 --- a/homeassistant/components/somfy/translations/ru.json +++ b/homeassistant/components/somfy/translations/ru.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } } diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index b4680cc06de..767abda2fd7 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -1,7 +1,7 @@ """Cover Platform for the Somfy MyLink component.""" import logging -from homeassistant.components.cover import ENTITY_ID_FORMAT, CoverDevice +from homeassistant.components.cover import ENTITY_ID_FORMAT, CoverEntity from homeassistant.util import slugify from . import CONF_DEFAULT_REVERSE, DATA_SOMFY_MYLINK @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(cover_list) -class SomfyShade(CoverDevice): +class SomfyShade(CoverEntity): """Object for controlling a Somfy cover.""" def __init__( diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 7b181d375a5..4a4332cb0a5 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -1 +1,50 @@ """The songpal component.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import CONF_ENDPOINT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SONGPAL_CONFIG_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENDPOINT): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [SONGPAL_CONFIG_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: OrderedDict) -> bool: + """Set up songpal environment.""" + conf = config.get(DOMAIN) + if conf is None: + return True + for config_entry in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry, + ), + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up songpal media player.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "media_player") + ) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload songpal media player.""" + return await hass.config_entries.async_forward_entry_unload(entry, "media_player") diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py new file mode 100644 index 00000000000..96e1e7ed7df --- /dev/null +++ b/homeassistant/components/songpal/config_flow.py @@ -0,0 +1,153 @@ +"""Config flow to configure songpal component.""" +import logging +from typing import Optional +from urllib.parse import urlparse + +from songpal import Device, SongpalException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST, CONF_NAME + +from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class SongpalConfig: + """Device Configuration.""" + + def __init__(self, name, host, endpoint): + """Initialize Configuration.""" + self.name = name + self.host = host + self.endpoint = endpoint + + +class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Songpal configuration flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the flow.""" + self.conf: Optional[SongpalConfig] = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_ENDPOINT): str}), + ) + + # Validate input + endpoint = user_input[CONF_ENDPOINT] + parsed_url = urlparse(endpoint) + + # Try to connect and get device name + try: + device = Device(endpoint) + await device.get_supported_methods() + interface_info = await device.get_interface_information() + name = interface_info.modelName + except SongpalException as ex: + _LOGGER.debug("Connection failed: %s", ex) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_ENDPOINT, default=user_input.get(CONF_ENDPOINT, "") + ): str, + } + ), + errors={"base": "cannot_connect"}, + ) + + self.conf = SongpalConfig(name, parsed_url.hostname, endpoint) + + return await self.async_step_init(user_input) + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + # Check if already configured + if self._endpoint_already_configured(): + return self.async_abort(reason="already_configured") + + if user_input is None: + return self.async_show_form( + step_id="init", + description_placeholders={ + CONF_NAME: self.conf.name, + CONF_HOST: self.conf.host, + }, + ) + + await self.async_set_unique_id(self.conf.endpoint) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.conf.name, + data={CONF_NAME: self.conf.name, CONF_ENDPOINT: self.conf.endpoint}, + ) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered Songpal device.""" + await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + self._abort_if_unique_id_configured() + + _LOGGER.debug("Discovered: %s", discovery_info) + + friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + scalarweb_info = discovery_info["X_ScalarWebAPI_DeviceInfo"] + endpoint = scalarweb_info["X_ScalarWebAPI_BaseURL"] + service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][ + "X_ScalarWebAPI_ServiceType" + ] + + # Ignore Bravia TVs + if "videoScreen" in service_types: + return self.async_abort(reason="not_songpal_device") + + # pylint: disable=no-member + self.context["title_placeholders"] = { + CONF_NAME: friendly_name, + CONF_HOST: parsed_url.hostname, + } + + self.conf = SongpalConfig(friendly_name, parsed_url.hostname, endpoint) + + return await self.async_step_init() + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + name = user_input.get(CONF_NAME) + endpoint = user_input.get(CONF_ENDPOINT) + parsed_url = urlparse(endpoint) + + # Try to connect to test the endpoint + try: + device = Device(endpoint) + await device.get_supported_methods() + # Get name + if name is None: + interface_info = await device.get_interface_information() + name = interface_info.modelName + except SongpalException as ex: + _LOGGER.error("Import from yaml configuration failed: %s", ex) + return self.async_abort(reason="cannot_connect") + + self.conf = SongpalConfig(name, parsed_url.hostname, endpoint) + + return await self.async_step_init(user_input) + + def _endpoint_already_configured(self): + """See if we already have an endpoint matching user input configured.""" + existing_endpoints = [ + entry.data[CONF_ENDPOINT] for entry in self._async_current_entries() + ] + return self.conf.endpoint in existing_endpoints diff --git a/homeassistant/components/songpal/const.py b/homeassistant/components/songpal/const.py index 6a19e316a9f..f12b77800a9 100644 --- a/homeassistant/components/songpal/const.py +++ b/homeassistant/components/songpal/const.py @@ -1,3 +1,5 @@ """Constants for the Songpal component.""" DOMAIN = "songpal" SET_SOUND_SETTING = "set_sound_setting" + +CONF_ENDPOINT = "endpoint" diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 583f0dff6ef..40df684df79 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -1,7 +1,15 @@ { "domain": "songpal", "name": "Sony Songpal", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.11.2"], - "codeowners": ["@rytilahti"] + "requirements": ["python-songpal==0.12"], + "codeowners": ["@rytilahti", "@shenxn"], + "ssdp": [ + { + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", + "manufacturer": "Sony Corporation" + } + ], + "quality_scale": "gold" } diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index d11ff84a73c..3777ecd8325 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -3,6 +3,7 @@ import asyncio from collections import OrderedDict import logging +import async_timeout from songpal import ( ConnectChange, ContentChange, @@ -13,7 +14,7 @@ from songpal import ( ) import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -22,27 +23,23 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_NAME, - EVENT_HOMEASSISTANT_STOP, - STATE_OFF, - STATE_ON, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_platform, +) +from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, SET_SOUND_SETTING +from .const import CONF_ENDPOINT, DOMAIN, SET_SOUND_SETTING _LOGGER = logging.getLogger(__name__) -CONF_ENDPOINT = "endpoint" - PARAM_NAME = "name" PARAM_VALUE = "value" -PLATFORM = "songpal" - SUPPORT_SONGPAL = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP @@ -52,81 +49,56 @@ SUPPORT_SONGPAL = ( | SUPPORT_TURN_OFF ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENDPOINT): cv.string} -) - -SET_SOUND_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(PARAM_NAME): cv.string, - vol.Required(PARAM_VALUE): cv.string, - } -) +INITIAL_RETRY_DELAY = 10 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Songpal platform.""" - if PLATFORM not in hass.data: - hass.data[PLATFORM] = {} - - if discovery_info is not None: - name = discovery_info["name"] - endpoint = discovery_info["properties"]["endpoint"] - _LOGGER.debug("Got autodiscovered %s - endpoint: %s", name, endpoint) - - device = SongpalDevice(name, endpoint) - else: - name = config.get(CONF_NAME) - endpoint = config.get(CONF_ENDPOINT) - device = SongpalDevice(name, endpoint, poll=False) - - if endpoint in hass.data[PLATFORM]: - _LOGGER.debug("The endpoint exists already, skipping setup.") - return - - try: - await device.initialize() - except SongpalException as ex: - _LOGGER.error("Unable to get methods from songpal: %s", ex) - raise PlatformNotReady - - hass.data[PLATFORM][endpoint] = device - - async_add_entities([device], True) - - async def async_service_handler(service): - """Service handler.""" - entity_id = service.data.get("entity_id", None) - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - - for device in hass.data[PLATFORM].values(): - if device.entity_id == entity_id or entity_id is None: - _LOGGER.debug( - "Calling %s (entity: %s) with params %s", service, entity_id, params - ) - - await device.async_set_sound_setting( - params[PARAM_NAME], params[PARAM_VALUE] - ) - - hass.services.async_register( - DOMAIN, SET_SOUND_SETTING, async_service_handler, schema=SET_SOUND_SCHEMA +async def async_setup_platform( + hass: HomeAssistantType, config: dict, async_add_entities, discovery_info=None +) -> None: + """Set up from legacy configuration file. Obsolete.""" + _LOGGER.error( + "Configuring Songpal through media_player platform is no longer supported. Convert to songpal platform or UI configuration." ) -class SongpalDevice(MediaPlayerDevice): +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up songpal media player.""" + name = config_entry.data[CONF_NAME] + endpoint = config_entry.data[CONF_ENDPOINT] + + device = Device(endpoint) + try: + async with async_timeout.timeout( + 10 + ): # set timeout to avoid blocking the setup process + await device.get_supported_methods() + except (SongpalException, asyncio.TimeoutError) as ex: + _LOGGER.warning("[%s(%s)] Unable to connect.", name, endpoint) + _LOGGER.debug("Unable to get methods from songpal: %s", ex) + raise PlatformNotReady + + songpal_entity = SongpalEntity(name, device) + async_add_entities([songpal_entity], True) + + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SET_SOUND_SETTING, + {vol.Required(PARAM_NAME): cv.string, vol.Required(PARAM_VALUE): cv.string}, + "async_set_sound_setting", + ) + + +class SongpalEntity(MediaPlayerEntity): """Class representing a Songpal device.""" - def __init__(self, name, endpoint, poll=False): + def __init__(self, name, device): """Init.""" self._name = name - self._endpoint = endpoint - self._poll = poll - self.dev = Device(self._endpoint) + self._dev = device self._sysinfo = None + self._model = None self._state = False self._available = False @@ -144,12 +116,15 @@ class SongpalDevice(MediaPlayerDevice): @property def should_poll(self): """Return True if the device should be polled.""" - return self._poll + return False - async def initialize(self): - """Initialize the device.""" - await self.dev.get_supported_methods() - self._sysinfo = await self.dev.get_system_info() + async def async_added_to_hass(self): + """Run when entity is added to hass.""" + await self.async_activate_websocket() + + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await self._dev.stop_listen_notifications() async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" @@ -164,7 +139,7 @@ class SongpalDevice(MediaPlayerDevice): async def _source_changed(content: ContentChange): _LOGGER.debug("Source changed: %s", content) if content.is_input: - self._active_source = self._sources[content.source] + self._active_source = self._sources[content.uri] _LOGGER.debug("New active source: %s", self._active_source) self.async_write_ha_state() else: @@ -176,40 +151,48 @@ class SongpalDevice(MediaPlayerDevice): self.async_write_ha_state() async def _try_reconnect(connect: ConnectChange): - _LOGGER.error( - "Got disconnected with %s, trying to reconnect.", connect.exception + _LOGGER.warning( + "[%s(%s)] Got disconnected, trying to reconnect.", + self.name, + self._dev.endpoint, ) + _LOGGER.debug("Disconnected: %s", connect.exception) self._available = False - self.dev.clear_notification_callbacks() self.async_write_ha_state() # Try to reconnect forever, a successful reconnect will initialize # the websocket connection again. - delay = 10 + delay = INITIAL_RETRY_DELAY while not self._available: _LOGGER.debug("Trying to reconnect in %s seconds", delay) await asyncio.sleep(delay) - # We need to inform HA about the state in case we are coming - # back from a disconnected state. - await self.async_update_ha_state(force_refresh=True) - delay = min(2 * delay, 300) - _LOGGER.info("Reconnected to %s", self.name) + try: + await self._dev.get_supported_methods() + except SongpalException as ex: + _LOGGER.debug("Failed to reconnect: %s", ex) + delay = min(2 * delay, 300) + else: + # We need to inform HA about the state in case we are coming + # back from a disconnected state. + await self.async_update_ha_state(force_refresh=True) - self.dev.on_notification(VolumeChange, _volume_changed) - self.dev.on_notification(ContentChange, _source_changed) - self.dev.on_notification(PowerChange, _power_changed) - self.dev.on_notification(ConnectChange, _try_reconnect) + self.hass.loop.create_task(self._dev.listen_notifications()) + _LOGGER.warning( + "[%s(%s)] Connection reestablished.", self.name, self._dev.endpoint + ) - async def listen_events(): - await self.dev.listen_notifications() + self._dev.on_notification(VolumeChange, _volume_changed) + self._dev.on_notification(ContentChange, _source_changed) + self._dev.on_notification(PowerChange, _power_changed) + self._dev.on_notification(ConnectChange, _try_reconnect) async def handle_stop(event): - await self.dev.stop_listen_notifications() + await self._dev.stop_listen_notifications() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) - self.hass.loop.create_task(listen_events()) + self.hass.loop.create_task(self._dev.listen_notifications()) @property def name(self): @@ -221,6 +204,18 @@ class SongpalDevice(MediaPlayerDevice): """Return a unique ID.""" return self._sysinfo.macAddr + @property + def device_info(self): + """Return the device info.""" + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self._sysinfo.macAddr)}, + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": "Sony Corporation", + "name": self.name, + "sw_version": self._sysinfo.version, + "model": self._model, + } + @property def available(self): """Return availability of the device.""" @@ -228,12 +223,20 @@ class SongpalDevice(MediaPlayerDevice): async def async_set_sound_setting(self, name, value): """Change a setting on the device.""" - await self.dev.set_sound_settings(name, value) + _LOGGER.debug("Calling set_sound_setting with %s: %s", name, value) + await self._dev.set_sound_settings(name, value) async def async_update(self): """Fetch updates from the device.""" try: - volumes = await self.dev.get_volume_information() + if self._sysinfo is None: + self._sysinfo = await self._dev.get_system_info() + + if self._model is None: + interface_info = await self._dev.get_interface_information() + self._model = interface_info.modelName + + volumes = await self._dev.get_volume_information() if not volumes: _LOGGER.error("Got no volume controls, bailing out") self._available = False @@ -251,11 +254,11 @@ class SongpalDevice(MediaPlayerDevice): self._volume_control = volume self._is_muted = self._volume_control.is_muted - status = await self.dev.get_power() + status = await self._dev.get_power() self._state = status.status _LOGGER.debug("Got state: %s", status) - inputs = await self.dev.get_inputs() + inputs = await self._dev.get_inputs() _LOGGER.debug("Got ins: %s", inputs) self._sources = OrderedDict() @@ -268,9 +271,6 @@ class SongpalDevice(MediaPlayerDevice): self._available = True - # activate notifications if wanted - if not self._poll: - await self.hass.async_create_task(self.async_activate_websocket()) except SongpalException as ex: _LOGGER.error("Unable to update: %s", ex) self._available = False @@ -324,11 +324,11 @@ class SongpalDevice(MediaPlayerDevice): async def async_turn_on(self): """Turn the device on.""" - return await self.dev.set_power(True) + return await self._dev.set_power(True) async def async_turn_off(self): """Turn the device off.""" - return await self.dev.set_power(False) + return await self._dev.set_power(False) async def async_mute_volume(self, mute): """Mute or unmute the device.""" diff --git a/homeassistant/components/songpal/strings.json b/homeassistant/components/songpal/strings.json new file mode 100644 index 00000000000..65c42ddef6a --- /dev/null +++ b/homeassistant/components/songpal/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "user": { + "data": { + "endpoint": "Endpoint" + } + }, + "init": { + "description": "Do you want to set up {name} ({host})?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_songpal_device": "Not a Songpal device" + } + } +} diff --git a/homeassistant/components/songpal/translations/ca.json b/homeassistant/components/songpal/translations/ca.json new file mode 100644 index 00000000000..9f7c18187c3 --- /dev/null +++ b/homeassistant/components/songpal/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "not_songpal_device": "No \u00e9s un dispositiu Songpal" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar", + "connection": "Error de connexi\u00f3: comprova l'endpoint seleccionat" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Vols configurar {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Endpoint" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/da.json b/homeassistant/components/songpal/translations/da.json new file mode 100644 index 00000000000..00c41b04a2e --- /dev/null +++ b/homeassistant/components/songpal/translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "title": "Sony Songpal" + }, + "user": { + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/de.json b/homeassistant/components/songpal/translations/de.json new file mode 100644 index 00000000000..b351dab97db --- /dev/null +++ b/homeassistant/components/songpal/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t bereits konfiguriert", + "not_songpal_device": "Kein Songpal-Ger\u00e4t" + }, + "error": { + "connection": "Verbindungsfehler: Bitte \u00fcberpr\u00fcfen Sie Ihren Endpunkt" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Endpunkt" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/en.json b/homeassistant/components/songpal/translations/en.json new file mode 100644 index 00000000000..f84510040ac --- /dev/null +++ b/homeassistant/components/songpal/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "not_songpal_device": "Not a Songpal device" + }, + "error": { + "cannot_connect": "Failed to connect", + "connection": "Connection error: please check your endpoint" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Do you want to set up {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Endpoint" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/es.json b/homeassistant/components/songpal/translations/es.json new file mode 100644 index 00000000000..bb82d0a006d --- /dev/null +++ b/homeassistant/components/songpal/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo ya configurado", + "not_songpal_device": "No es un dispositivo Songpal" + }, + "error": { + "cannot_connect": "Error al conectar", + "connection": "Error de conexi\u00f3n: comprueba tu endpoint" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "\u00bfQuieres configurar {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Endpoint" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/fi.json b/homeassistant/components/songpal/translations/fi.json new file mode 100644 index 00000000000..3e06c6f1461 --- /dev/null +++ b/homeassistant/components/songpal/translations/fi.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty", + "not_songpal_device": "Ei Songpal-laite" + }, + "error": { + "connection": "Yhteysvirhe: tarkista p\u00e4\u00e4tepisteesi" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 kohteen {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "P\u00e4\u00e4tepiste" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/fr.json b/homeassistant/components/songpal/translations/fr.json new file mode 100644 index 00000000000..f8a278a288e --- /dev/null +++ b/homeassistant/components/songpal/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Appareil d\u00e9j\u00e0 configur\u00e9", + "not_songpal_device": "Pas un appareil Songpal" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Voulez-vous configurer {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/he.json b/homeassistant/components/songpal/translations/he.json new file mode 100644 index 00000000000..b18a6bbcd3a --- /dev/null +++ b/homeassistant/components/songpal/translations/he.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u05de\u05db\u05e9\u05d9\u05e8 \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json new file mode 100644 index 00000000000..b70fdd2d9b5 --- /dev/null +++ b/homeassistant/components/songpal/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "endpoint": "V\u00e9gpont" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/it.json b/homeassistant/components/songpal/translations/it.json new file mode 100644 index 00000000000..6e0eb1fdc94 --- /dev/null +++ b/homeassistant/components/songpal/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "not_songpal_device": "Non \u00e8 un dispositivo Songpal" + }, + "error": { + "connection": "Errore di connessione: controlla il tuo endpoint" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Vuoi impostare {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Endpoint" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/ko.json b/homeassistant/components/songpal/translations/ko.json new file mode 100644 index 00000000000..b71ca83bf38 --- /dev/null +++ b/homeassistant/components/songpal/translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_songpal_device": "Songpal \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "connection": "\uc5f0\uacb0 \uc624\ub958: \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "\uc5d4\ub4dc\ud3ec\uc778\ud2b8" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/lb.json b/homeassistant/components/songpal/translations/lb.json new file mode 100644 index 00000000000..c3d31b81e53 --- /dev/null +++ b/homeassistant/components/songpal/translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "not_songpal_device": "Keen Songpal Apparat" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "connection": "Feeler beim verbannen Iwwerpr\u00e9if w.e.g. den Endpunkt" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Endpunkt" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/no.json b/homeassistant/components/songpal/translations/no.json new file mode 100644 index 00000000000..adbee2adc9f --- /dev/null +++ b/homeassistant/components/songpal/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "not_songpal_device": "Ikke en Songpal-enhet" + }, + "error": { + "connection": "Tilkoblingsfeil: vennligst sjekk endepunktet ditt" + }, + "flow_title": "", + "step": { + "init": { + "description": "Vil du sette opp {name} ({host})?", + "title": "" + }, + "user": { + "data": { + "endpoint": "Endepunkt" + }, + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json new file mode 100644 index 00000000000..8d6c913c844 --- /dev/null +++ b/homeassistant/components/songpal/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "not_songpal_device": "To nie jest urz\u0105dzenie Songpal" + }, + "error": { + "cannot_connect": "[%key_id:common::config_flow::error::cannot_connect%]", + "connection": "B\u0142\u0105d po\u0142\u0105czenia: sprawd\u017a punkt ko\u0144cowy" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Punkt ko\u0144cowy" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/ru.json b/homeassistant/components/songpal/translations/ru.json new file mode 100644 index 00000000000..3f9fc7ecc08 --- /dev/null +++ b/homeassistant/components/songpal/translations/ru.json @@ -0,0 +1,25 @@ +{ + "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.", + "not_songpal_device": "\u041d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Songpal." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "connection": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443." + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/sv.json b/homeassistant/components/songpal/translations/sv.json new file mode 100644 index 00000000000..9913f89f3d9 --- /dev/null +++ b/homeassistant/components/songpal/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_songpal_device": "Inte en Songpal-enhet" + }, + "error": { + "connection": "Anslutningsfel: Kontrollera destinationen" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Do vill du konfigurera {name} ({host})?", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "Destination." + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/zh-Hant.json b/homeassistant/components/songpal/translations/zh-Hant.json new file mode 100644 index 00000000000..c505979b85e --- /dev/null +++ b/homeassistant/components/songpal/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_songpal_device": "\u4e26\u975e Songpal \u8a2d\u5099" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "connection": "\u9023\u7dda\u932f\u8aa4\uff1a\u8acb\u6aa2\u67e5\u7aef\u9ede" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", + "title": "Sony Songpal" + }, + "user": { + "data": { + "endpoint": "\u7aef\u9ede" + }, + "title": "Sony Songpal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index ee035fb59c1..e5ce9ede290 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.25"], + "requirements": ["pysonos==0.0.30"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 634952dcfdc..b4dc9530b90 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -13,7 +13,7 @@ import pysonos.music_library import pysonos.snapshot import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, @@ -31,7 +31,13 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ( + ATTR_TIME, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.util.dt import utcnow @@ -99,11 +105,13 @@ UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} class SonosData: """Storage class for platform global data.""" - def __init__(self, hass): + def __init__(self): """Initialize the data.""" self.entities = [] self.discovered = [] self.topology_condition = asyncio.Condition() + self.discovery_thread = None + self.hosts_heartbeat = None async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -116,7 +124,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" if DATA_SONOS not in hass.data: - hass.data[DATA_SONOS] = SonosData(hass) + hass.data[DATA_SONOS] = SonosData() config = hass.data[SONOS_DOMAIN].get("media_player", {}) _LOGGER.debug("Reached async_setup_entry, config=%s", config) @@ -125,6 +133,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr + def _stop_discovery(event): + data = hass.data[DATA_SONOS] + if data.discovery_thread: + data.discovery_thread.stop() + data.discovery_thread = None + if data.hosts_heartbeat: + data.hosts_heartbeat() + data.hosts_heartbeat = None + def _discovery(now=None): """Discover players from network or configuration.""" hosts = config.get(CONF_HOSTS) @@ -162,10 +179,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.warning("Failed to initialize '%s'", host) _LOGGER.debug("Tested all hosts") - hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery) + hass.data[DATA_SONOS].hosts_heartbeat = hass.helpers.event.call_later( + DISCOVERY_INTERVAL, _discovery + ) else: _LOGGER.debug("Starting discovery thread") - pysonos.discover_thread( + hass.data[DATA_SONOS].discovery_thread = pysonos.discover_thread( _discovered_player, interval=DISCOVERY_INTERVAL, interface_addr=config.get(CONF_INTERFACE_ADDR), @@ -173,6 +192,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.debug("Adding discovery job") hass.async_add_executor_job(_discovery) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) platform = entity_platform.current_platform.get() @@ -305,9 +325,7 @@ def soco_error(errorcodes=None): try: return funct(*args, **kwargs) except SoCoUPnPException as err: - if errorcodes and err.error_code in errorcodes: - pass - else: + if not errorcodes or err.error_code not in errorcodes: _LOGGER.error("Error on %s with %s", funct.__name__, err) except SoCoException as err: _LOGGER.error("Error on %s with %s", funct.__name__, err) @@ -338,7 +356,7 @@ def _timespan_secs(timespan): return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) -class SonosEntity(MediaPlayerDevice): +class SonosEntity(MediaPlayerEntity): """Representation of a Sonos entity.""" def __init__(self, player): @@ -585,7 +603,6 @@ class SonosEntity(MediaPlayerDevice): variables = event and event.variables self.update_media_radio(variables, track_info) else: - variables = event and event.variables self.update_media_music(update_position, track_info) self.schedule_update_ha_state() @@ -639,17 +656,14 @@ class SonosEntity(MediaPlayerDevice): def update_media_music(self, update_media_position, track_info): """Update state when playing music tracks.""" self._media_duration = _timespan_secs(track_info.get("duration")) - - position_info = self.soco.avTransport.GetPositionInfo( - [("InstanceID", 0), ("Channel", "Master")] - ) - rel_time = _timespan_secs(position_info.get("RelTime")) + current_position = _timespan_secs(track_info.get("position")) # player started reporting position? - update_media_position |= rel_time is not None and self._media_position is None + if current_position is not None and self._media_position is None: + update_media_position = True # position jumped? - if rel_time is not None and self._media_position is not None: + if current_position is not None and self._media_position is not None: if self.state == STATE_PLAYING: time_diff = utcnow() - self._media_position_updated_at time_diff = time_diff.total_seconds() @@ -658,12 +672,13 @@ class SonosEntity(MediaPlayerDevice): calculated_position = self._media_position + time_diff - update_media_position |= abs(calculated_position - rel_time) > 1.5 + if abs(calculated_position - current_position) > 1.5: + update_media_position = True - if rel_time is None: + if current_position is None: self._clear_media_position() elif update_media_position: - self._media_position = rel_time + self._media_position = current_position self._media_position_updated_at = utcnow() self._media_image_url = track_info.get("album_art") diff --git a/homeassistant/components/sonos/translations/fi.json b/homeassistant/components/sonos/translations/fi.json new file mode 100644 index 00000000000..a01f678b03c --- /dev/null +++ b/homeassistant/components/sonos/translations/fi.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 Sonosin?", + "title": "Sonos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/no.json b/homeassistant/components/sonos/translations/no.json index 5e6560a294a..db792405988 100644 --- a/homeassistant/components/sonos/translations/no.json +++ b/homeassistant/components/sonos/translations/no.json @@ -7,7 +7,7 @@ "step": { "confirm": { "description": "\u00d8nsker du \u00e5 sette opp Sonos?", - "title": "Sonos" + "title": "" } } } diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index e68bed34cfa..e14d74dd2c0 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -4,7 +4,7 @@ import logging import pysdcp 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_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class SonyProjector(SwitchDevice): +class SonyProjector(SwitchEntity): """Represents a Sony Projector as a switch.""" def __init__(self, sdcp_connection, name): diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 6477983d94f..c9cc4f32734 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -2,6 +2,6 @@ "domain": "soundtouch", "name": "Bose Soundtouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", - "requirements": ["libsoundtouch==0.7.2"], + "requirements": ["libsoundtouch==0.8"], "codeowners": [] } diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 2f64a2d3605..83c8192ccb2 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -3,15 +3,17 @@ import logging import re from libsoundtouch import soundtouch_device +from libsoundtouch.utils import Source import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -80,6 +82,7 @@ SUPPORT_SOUNDTOUCH = ( | SUPPORT_TURN_ON | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + | SUPPORT_SELECT_SOURCE ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -184,7 +187,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class SoundTouchDevice(MediaPlayerDevice): +class SoundTouchDevice(MediaPlayerEntity): """Representation of a SoundTouch Bose device.""" def __init__(self, name, config): @@ -234,6 +237,19 @@ class SoundTouchDevice(MediaPlayerDevice): return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) + @property + def source(self): + """Name of the current input source.""" + return self._status.source + + @property + def source_list(self): + """List of available input sources.""" + return [ + Source.AUX.value, + Source.BLUETOOTH.value, + ] + @property def is_volume_muted(self): """Boolean if volume is currently muted.""" @@ -357,6 +373,17 @@ class SoundTouchDevice(MediaPlayerDevice): else: _LOGGER.warning("Unable to find preset with id %s", media_id) + def select_source(self, source): + """Select input source.""" + if source == Source.AUX.value: + _LOGGER.debug("Selecting source AUX") + self._device.select_source_aux() + elif source == Source.BLUETOOTH.value: + _LOGGER.debug("Selecting source Bluetooth") + self._device.select_source_bluetooth() + else: + _LOGGER.warning("Source %s is not supported", source) + def create_zone(self, slaves): """ Create a zone (multi-room) and play on selected devices. diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index 982c0fe2bab..86adc588038 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -47,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([SpcAlarm(area=area, api=api) for area in api.areas.values()]) -class SpcAlarm(alarm.AlarmControlPanel): +class SpcAlarm(alarm.AlarmControlPanelEntity): """Representation of the SPC alarm panel.""" def __init__(self, area, api): diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 626e30849df..75256b60cfb 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -3,7 +3,7 @@ import logging from pyspcwebgw.const import ZoneInput, ZoneType -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -35,7 +35,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class SpcBinarySensor(BinarySensorDevice): +class SpcBinarySensor(BinarySensorEntity): """Representation of a sensor based on a SPC zone.""" def __init__(self, zone): diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 846ed6a1ac6..78c77f3679a 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -2,7 +2,7 @@ import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -41,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class SpiderThermostat(ClimateDevice): +class SpiderThermostat(ClimateEntity): """Representation of a thermostat.""" def __init__(self, api, thermostat): diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index b02c38c84aa..58a45cf7b4d 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -1,7 +1,7 @@ """Support for Spider switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DOMAIN as SPIDER_DOMAIN @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class SpiderPowerPlug(SwitchDevice): +class SpiderPowerPlug(SwitchEntity): """Representation of a Spider Power Plug.""" def __init__(self, api, power_plug): diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index bbbfb75a536..88e0c938a28 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.11.1"], + "requirements": ["spotipy==2.12.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7a00fb02146..1b74855c9f9 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -9,7 +9,7 @@ from aiohttp import ClientError from spotipy import Spotify, SpotifyException from yarl import URL -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -32,6 +32,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp @@ -90,10 +91,17 @@ def spotify_exception_handler(func): return wrapper -class SpotifyMediaPlayer(MediaPlayerDevice): +class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" - def __init__(self, session, spotify: Spotify, me: dict, user_id: str, name: str): + def __init__( + self, + session: OAuth2Session, + spotify: Spotify, + me: dict, + user_id: str, + name: str, + ): """Initialize.""" self._id = user_id self._me = me diff --git a/homeassistant/components/spotify/translations/es-419.json b/homeassistant/components/spotify/translations/es-419.json new file mode 100644 index 00000000000..f9b956e47bc --- /dev/null +++ b/homeassistant/components/spotify/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta de Spotify.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Spotify." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json index 4892d7a45c0..deb55479c1e 100644 --- a/homeassistant/components/spotify/translations/ko.json +++ b/homeassistant/components/spotify/translations/ko.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index 35b3e4c45f5..c6e009c00c5 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere en Spotify-konto.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", "missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen." }, "create_entry": { - "default": "Vellykket autentisering med Spotify." + "default": "Vellykket godkjenning med Spotify." }, "step": { "pick_implementation": { - "title": "Velg autentiseringsmetode" + "title": "Velg godkjenningsmetode" } } } diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index ca93393c779..f3c8413a02a 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Spotify.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "Integracja ze Spotify nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelnienia" + "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" } } } diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index aa14f158300..5738707a3a4 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } } diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 1e8fd6f3a2a..e7e52fe2d80 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -1,3 +1,10 @@ """Constants for the Squeezebox component.""" +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING + DOMAIN = "squeezebox" SERVICE_CALL_METHOD = "call_method" +SQUEEZEBOX_MODE = { + "pause": STATE_PAUSED, + "play": STATE_PLAYING, + "stop": STATE_IDLE, +} diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index bbd32e9eefe..ae076c88b4a 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -2,5 +2,6 @@ "domain": "squeezebox", "name": "Logitech Squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox", - "codeowners": [] + "codeowners": ["@rajlaud"], + "requirements": ["pysqueezebox==0.1.4"] } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 57305a3b4c0..7194959d990 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -1,15 +1,11 @@ """Support for interfacing to the Logitech SqueezeBox API.""" -import asyncio -import json import logging import socket -import urllib.parse -import aiohttp -import async_timeout +from pysqueezebox import Server import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, @@ -28,23 +24,26 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import ( ATTR_COMMAND, - ATTR_ENTITY_ID, CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - HTTP_OK, - STATE_IDLE, STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -from .const import DOMAIN, SERVICE_CALL_METHOD +from .const import SQUEEZEBOX_MODE + +SERVICE_CALL_METHOD = "call_method" +SERVICE_CALL_QUERY = "call_query" +SERVICE_SYNC = "sync" +SERVICE_UNSYNC = "unsync" + +ATTR_QUERY_RESULT = "query_result" +ATTR_SYNC_GROUP = "sync_group" _LOGGER = logging.getLogger(__name__) @@ -66,8 +65,6 @@ SUPPORT_SQUEEZEBOX = ( | SUPPORT_CLEAR_PLAYLIST ) -MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -83,21 +80,12 @@ KNOWN_SERVERS = "squeezebox_known_servers" ATTR_PARAMETERS = "parameters" -SQUEEZEBOX_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): cv.string, - vol.Optional(ATTR_PARAMETERS): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ), - } -) +ATTR_OTHER_PLAYER = "other_player" -SERVICE_TO_METHOD = { - SERVICE_CALL_METHOD: { - "method": "async_call_method", - "schema": SQUEEZEBOX_CALL_METHOD_SCHEMA, - } -} +ATTR_TO_PROPERTY = [ + ATTR_QUERY_RESULT, + ATTR_SYNC_GROUP, +] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -126,7 +114,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Get IP of host, to prevent duplication of same host (different DNS names) try: - ipaddr = socket.gethostbyname(host) + ipaddr = await hass.async_add_executor_job(socket.gethostbyname, host) except OSError as error: _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) raise PlatformNotReady from error @@ -135,180 +123,99 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return _LOGGER.debug("Creating LMS object for %s", ipaddr) - lms = LogitechMediaServer(hass, host, port, username, password) - - players = await lms.create_players() - if players is None: - raise PlatformNotReady - + lms = Server(async_get_clientsession(hass), host, port, username, password) known_servers.add(ipaddr) - hass.data[DATA_SQUEEZEBOX].extend(players) - async_add_entities(players) + players = await lms.async_get_players() + if players is None: + raise PlatformNotReady + media_players = [] + for player in players: + media_players.append(SqueezeBoxDevice(player)) - async def async_service_handler(service): - """Map services to methods on MediaPlayerDevice.""" - method = SERVICE_TO_METHOD.get(service.service) - if not method: - return + hass.data[DATA_SQUEEZEBOX].extend(media_players) + async_add_entities(media_players) - params = { - key: value for key, value in service.data.items() if key != "entity_id" - } - entity_ids = service.data.get("entity_id") - if entity_ids: - target_players = [ - player - for player in hass.data[DATA_SQUEEZEBOX] - if player.entity_id in entity_ids - ] - else: - target_players = hass.data[DATA_SQUEEZEBOX] + platform = entity_platform.current_platform.get() - update_tasks = [] - for player in target_players: - await getattr(player, method["method"])(**params) - update_tasks.append(player.async_update_ha_state(True)) + platform.async_register_entity_service( + SERVICE_CALL_METHOD, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMETERS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + }, + "async_call_method", + ) - if update_tasks: - await asyncio.wait(update_tasks) + platform.async_register_entity_service( + SERVICE_CALL_QUERY, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMETERS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + }, + "async_call_query", + ) - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema - ) + platform.async_register_entity_service( + SERVICE_SYNC, {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", + ) + + platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") return True -class LogitechMediaServer: - """Representation of a Logitech media server.""" +class SqueezeBoxDevice(MediaPlayerEntity): + """ + Representation of a SqueezeBox device. - def __init__(self, hass, host, port, username, password): - """Initialize the Logitech device.""" - self.hass = hass - self.host = host - self.port = port - self._username = username - self._password = password + Wraps a pysqueezebox.Player() object. + """ - async def create_players(self): - """Create a list of devices connected to LMS.""" - result = [] - data = await self.async_query("players", "status") - if data is False: - return None - for players in data.get("players_loop", []): - player = SqueezeBoxDevice(self, players["playerid"], players["name"]) - await player.async_update() - result.append(player) - return result - - async def async_query(self, *command, player=""): - """Abstract out the JSON-RPC connection.""" - auth = ( - None - if self._username is None - else aiohttp.BasicAuth(self._username, self._password) - ) - url = f"http://{self.host}:{self.port}/jsonrpc.js" - data = json.dumps( - {"id": "1", "method": "slim.request", "params": [player, command]} - ) - - _LOGGER.debug("URL: %s Data: %s", url, data) - - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(TIMEOUT): - response = await websession.post(url, data=data, auth=auth) - - if response.status != HTTP_OK: - _LOGGER.error( - "Query failed, response code: %s Full message: %s", - response.status, - response, - ) - return False - - data = await response.json() - - except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed communicating with LMS: %s", type(error)) - return False - - try: - return data["result"] - except AttributeError: - _LOGGER.error("Received invalid response: %s", data) - return False - - -class SqueezeBoxDevice(MediaPlayerDevice): - """Representation of a SqueezeBox device.""" - - def __init__(self, lms, player_id, name): + def __init__(self, player): """Initialize the SqueezeBox device.""" - super().__init__() - self._lms = lms - self._id = player_id - self._status = {} - self._name = name + self._player = player self._last_update = None - _LOGGER.debug("Creating SqueezeBox object: %s, %s", name, player_id) + self._query_result = {} + + @property + def device_state_attributes(self): + """Return device-specific attributes.""" + squeezebox_attr = { + attr: getattr(self, attr) + for attr in ATTR_TO_PROPERTY + if getattr(self, attr) is not None + } + + return squeezebox_attr @property def name(self): """Return the name of the device.""" - return self._name + return self._player.name @property def unique_id(self): """Return a unique ID.""" - return self._id + return self._player.player_id @property def state(self): """Return the state of the device.""" - if "power" in self._status and self._status["power"] == 0: + if self._player.power is not None and not self._player.power: return STATE_OFF - if "mode" in self._status: - if self._status["mode"] == "pause": - return STATE_PAUSED - if self._status["mode"] == "play": - return STATE_PLAYING - if self._status["mode"] == "stop": - return STATE_IDLE + if self._player.mode: + return SQUEEZEBOX_MODE.get(self._player.mode) return None - async def async_query(self, *parameters): - """Send a command to the LMS.""" - return await self._lms.async_query(*parameters, player=self._id) - async def async_update(self): - """Retrieve the current state of the player.""" - tags = "adKl" - response = await self.async_query("status", "-", "1", f"tags:{tags}") - - if response is False: - return - + """Update the Player() object.""" last_media_position = self.media_position - - self._status = {} - - try: - self._status.update(response["playlist_loop"][0]) - except KeyError: - pass - try: - self._status.update(response["remoteMeta"]) - except KeyError: - pass - - self._status.update(response) - + await self._player.async_update() if self.media_position != last_media_position: _LOGGER.debug( "Media position updated for %s: %s", self, self.media_position @@ -318,20 +225,18 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - if "mixer volume" in self._status: - return int(float(self._status["mixer volume"])) / 100.0 + if self._player.volume: + return int(float(self._player.volume)) / 100.0 @property def is_volume_muted(self): """Return true if volume is muted.""" - if "mixer volume" in self._status: - return str(self._status["mixer volume"]).startswith("-") + return self._player.muting @property def media_content_id(self): """Content ID of current playing media.""" - if "current_title" in self._status: - return self._status["current_title"] + return self._player.url @property def media_content_type(self): @@ -341,14 +246,12 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def media_duration(self): """Duration of current playing media in seconds.""" - if "duration" in self._status: - return int(float(self._status["duration"])) + return self._player.duration @property def media_position(self): - """Duration of current playing media in seconds.""" - if "time" in self._status: - return int(float(self._status["time"])) + """Position of current playing media in seconds.""" + return self._player.time @property def media_position_updated_at(self): @@ -358,115 +261,96 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def media_image_url(self): """Image url of current playing media.""" - if "artwork_url" in self._status: - media_url = self._status["artwork_url"] - elif "id" in self._status: - media_url = ("/music/{track_id}/cover.jpg").format( - track_id=self._status["id"] - ) - else: - media_url = ("/music/current/cover.jpg?player={player}").format( - player=self._id - ) - - # pylint: disable=protected-access - if self._lms._username: - base_url = "http://{username}:{password}@{server}:{port}/".format( - username=self._lms._username, - password=self._lms._password, - server=self._lms.host, - port=self._lms.port, - ) - else: - base_url = "http://{server}:{port}/".format( - server=self._lms.host, port=self._lms.port - ) - - url = urllib.parse.urljoin(base_url, media_url) - - return url + return self._player.image_url @property def media_title(self): """Title of current playing media.""" - if "title" in self._status: - return self._status["title"] - - if "current_title" in self._status: - return self._status["current_title"] + return self._player.title @property def media_artist(self): """Artist of current playing media.""" - if "artist" in self._status: - return self._status["artist"] + return self._player.artist @property def media_album_name(self): """Album of current playing media.""" - if "album" in self._status: - return self._status["album"] + return self._player.album @property def shuffle(self): """Boolean if shuffle is enabled.""" - if "playlist_shuffle" in self._status: - return self._status["playlist_shuffle"] == 1 + return self._player.shuffle @property def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_SQUEEZEBOX + @property + def sync_group(self): + """List players we are synced with.""" + player_ids = {p.unique_id: p.entity_id for p in self.hass.data[DATA_SQUEEZEBOX]} + sync_group = [] + for player in self._player.sync_group: + if player in player_ids: + sync_group.append(player_ids[player]) + return sync_group + + @property + def query_result(self): + """Return the result from the call_query service.""" + return self._query_result + async def async_turn_off(self): """Turn off media player.""" - await self.async_query("power", "0") + await self._player.async_set_power(False) async def async_volume_up(self): """Volume up media player.""" - await self.async_query("mixer", "volume", "+5") + await self._player.async_set_volume("+5") async def async_volume_down(self): """Volume down media player.""" - await self.async_query("mixer", "volume", "-5") + await self._player.async_set_volume("-5") async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" volume_percent = str(int(volume * 100)) - await self.async_query("mixer", "volume", volume_percent) + await self._player.async_set_volume(volume_percent) async def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - mute_numeric = "1" if mute else "0" - await self.async_query("mixer", "muting", mute_numeric) + await self._player.async_set_muting(mute) async def async_media_play_pause(self): """Send pause command to media player.""" - await self.async_query("pause") + await self._player.async_toggle_pause() async def async_media_play(self): """Send play command to media player.""" - await self.async_query("play") + await self._player.async_play() async def async_media_pause(self): """Send pause command to media player.""" - await self.async_query("pause", "1") + await self._player.async_pause() async def async_media_next_track(self): """Send next track command.""" - await self.async_query("playlist", "index", "+1") + await self._player.async_index("+1") async def async_media_previous_track(self): """Send next track command.""" - await self.async_query("playlist", "index", "-1") + await self._player.async_index("-1") async def async_media_seek(self, position): """Send seek command.""" - await self.async_query("time", position) + await self._player.async_time(position) async def async_turn_on(self): """Turn the media player on.""" - await self.async_query("power", "1") + await self._player.async_set_power(True) async def async_play_media(self, media_type, media_id, **kwargs): """ @@ -474,27 +358,20 @@ class SqueezeBoxDevice(MediaPlayerDevice): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. """ + cmd = "play" if kwargs.get(ATTR_MEDIA_ENQUEUE): - await self._add_uri_to_playlist(media_id) - return + cmd = "add" - await self._play_uri(media_id) - - async def _play_uri(self, media_id): - """Replace the current play list with the uri.""" - await self.async_query("playlist", "play", media_id) - - async def _add_uri_to_playlist(self, media_id): - """Add an item to the existing playlist.""" - await self.async_query("playlist", "add", media_id) + await self._player.async_load_url(media_id, cmd) async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" - await self.async_query("playlist", "shuffle", int(shuffle)) + shuffle_mode = "song" if shuffle else "none" + await self._player.async_set_shuffle(shuffle_mode) async def async_clear_playlist(self): """Send the media player the command for clear playlist.""" - await self.async_query("playlist", "clear") + await self._player.async_clear_playlist() async def async_call_method(self, command, parameters=None): """ @@ -507,4 +384,36 @@ class SqueezeBoxDevice(MediaPlayerDevice): if parameters: for parameter in parameters: all_params.append(parameter) - await self.async_query(*all_params) + await self._player.async_query(*all_params) + + async def async_call_query(self, command, parameters=None): + """ + Call Squeezebox JSON/RPC method where we care about the result. + + Additional parameters are added to the command to form the list of + positional parameters (p0, p1..., pN) passed to JSON/RPC server. + """ + all_params = [command] + if parameters: + for parameter in parameters: + all_params.append(parameter) + self._query_result = await self._player.async_query(*all_params) + _LOGGER.debug("call_query got result %s", self._query_result) + + async def async_sync(self, other_player): + """ + Add another Squeezebox player to this player's sync group. + + If the other player is a member of a sync group, it will leave the current sync group + without asking. + """ + player_ids = {p.entity_id: p.unique_id for p in self.hass.data[DATA_SQUEEZEBOX]} + other_player_id = player_ids.get(other_player) + if other_player_id: + await self._player.async_sync(other_player_id) + else: + _LOGGER.info("Could not find player_id for %s. Not syncing.", other_player) + + async def async_unsync(self): + """Unsync this Squeezebox player.""" + await self._player.async_unsync() diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index b1768949258..ef69ea67dbf 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -8,5 +8,35 @@ call_method: description: Command to pass to Logitech Media Server (p0 in the CLI documentation). example: "playlist" parameters: - description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). - example: ["loadtracks", "album.titlesearch="] + description: > + Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). + example: '["loadtracks", "album.titlesearch=Revolver"]' +call_query: + description: > + Call a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity. + fields: + entity_id: + description: Name(s) of the Squeezebox entities where to run the API method. + example: 'media_player.squeezebox_radio' + command: + description: Command to pass to Logitech Media Server (p0 in the CLI documentation). + example: 'albums' + parameters: + description: > + Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). + example: '["0", "20", "search:Revolver"]' +sync: + description: > + Add another player to this player's sync group. If the other player is already in a sync group, it will leave it. + fields: + entity_id: + description: Name of the Squeezebox entity where to run the API method. + example: "media_player.bedroom" + other_player: + description: Name of the other Squeezebox player to link. + example: "media_player.living_room" +unsync: + description: Remove this player from its sync group. + fields: + entity_id: + description: Name of the Squeezebox entity to unsync. diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index f2288e9363b..df9bd348b8d 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_LOCK, DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, - BinarySensorDevice, + BinarySensorEntity, ) from .account import StarlineAccount, StarlineDevice @@ -33,7 +33,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class StarlineSensor(StarlineEntity, BinarySensorDevice): +class StarlineSensor(StarlineEntity, BinarySensorEntity): """Representation of a StarLine binary sensor.""" def __init__( diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 804e8c8df2d..56cd8686186 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -1,5 +1,5 @@ """Support for StarLine lock.""" -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from .account import StarlineAccount, StarlineDevice from .const import DOMAIN @@ -19,7 +19,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class StarlineLock(StarlineEntity, LockDevice): +class StarlineLock(StarlineEntity, LockEntity): """Representation of a StarLine lock.""" def __init__(self, account: StarlineAccount, device: StarlineDevice): diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index 41d303b7876..33e28f9a29d 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -3,23 +3,33 @@ "step": { "auth_app": { "title": "Application credentials", - "description": "Application ID and secret code from StarLine developer account", - "data": { "app_id": "App ID", "app_secret": "Secret" } + "description": "Application ID and secret code from [StarLine developer account](https://my.starline.ru/developer)", + "data": { + "app_id": "App ID", + "app_secret": "Secret" + } }, "auth_user": { "title": "User credentials", "description": "StarLine account email and password", - "data": { "username": "Username", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } }, "auth_mfa": { "title": "Two-factor authorization", "description": "Enter the code sent to phone {phone_number}", - "data": { "mfa_code": "SMS code" } + "data": { + "mfa_code": "SMS code" + } }, "auth_captcha": { "title": "Captcha", "description": "{captcha_img}", - "data": { "captcha_code": "Code from image" } + "data": { + "captcha_code": "Code from image" + } } }, "error": { @@ -28,4 +38,4 @@ "error_auth_mfa": "Incorrect code" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index 920fe686d9a..c50a7bb4973 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -1,5 +1,5 @@ """Support for StarLine switch.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from .account import StarlineAccount, StarlineDevice from .const import DOMAIN @@ -30,7 +30,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class StarlineSwitch(StarlineEntity, SwitchDevice): +class StarlineSwitch(StarlineEntity, SwitchEntity): """Representation of a StarLine switch.""" def __init__( diff --git a/homeassistant/components/starline/translations/ca.json b/homeassistant/components/starline/translations/ca.json index d8c76856480..722b65da2a8 100644 --- a/homeassistant/components/starline/translations/ca.json +++ b/homeassistant/components/starline/translations/ca.json @@ -34,7 +34,7 @@ "username": "Nom d'usuari" }, "description": "Correu electr\u00f2nic i contrasenya del compte StarLine", - "title": "Credencials d\u2019usuari" + "title": "Credencials d'usuari" } } } diff --git a/homeassistant/components/starline/translations/es-419.json b/homeassistant/components/starline/translations/es-419.json new file mode 100644 index 00000000000..d6cdfa31b13 --- /dev/null +++ b/homeassistant/components/starline/translations/es-419.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "error_auth_app": "Identificaci\u00f3n de aplicaci\u00f3n incorrecta o secreto", + "error_auth_mfa": "C\u00f3digo incorrecto", + "error_auth_user": "Nombre de usuario o contrase\u00f1a incorrecta" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de la aplicaci\u00f3n", + "app_secret": "Secreto" + }, + "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de cuenta de desarrollador de StarLine ", + "title": "Credenciales de solicitud" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo de imagen" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Ingrese el c\u00f3digo enviado al tel\u00e9fono {phone_number}", + "title": "Autorizaci\u00f3n de dos factores" + }, + "auth_user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", + "title": "Credenciales de usuario" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/ko.json b/homeassistant/components/starline/translations/ko.json index 6d9e06c5a3d..a4afdd6e22f 100644 --- a/homeassistant/components/starline/translations/ko.json +++ b/homeassistant/components/starline/translations/ko.json @@ -25,7 +25,7 @@ "data": { "mfa_code": "SMS \ucf54\ub4dc" }, - "description": "{phone_number} \uc804\ud654\ub85c \uc804\uc1a1\ub41c \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "description": "{phone_number} \ubc88\ud638\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "2\ub2e8\uacc4 \uc778\uc99d" }, "auth_user": { diff --git a/homeassistant/components/starline/translations/no.json b/homeassistant/components/starline/translations/no.json index e17bd9ad95e..a96b788c90d 100644 --- a/homeassistant/components/starline/translations/no.json +++ b/homeassistant/components/starline/translations/no.json @@ -11,22 +11,22 @@ "app_id": "App-ID", "app_secret": "Hemmelig" }, - "description": "S\u00f8knads-ID og hemmelig kode fra StarLine utviklerkonto ", - "title": "Bruksanvisning" + "description": "Applikasjons-ID og hemmelig kode fra StarLine utviklerkonto ", + "title": "Programlegitimasjon" }, "auth_captcha": { "data": { "captcha_code": "Kode fra bilde" }, - "description": "{captcha_img}", - "title": "Captcha" + "description": "", + "title": "" }, "auth_mfa": { "data": { "mfa_code": "SMS-kode" }, - "description": "Skriv inn koden som er sendt til telefonen {phone_number}", - "title": "Tofaktorautentisering" + "description": "Angi koden som er sendt til telefonen {phone_number}", + "title": "Totrinnsbekreftelse" }, "auth_user": { "data": { diff --git a/homeassistant/components/starline/translations/pl.json b/homeassistant/components/starline/translations/pl.json index 5e5a293fc82..69691db21f8 100644 --- a/homeassistant/components/starline/translations/pl.json +++ b/homeassistant/components/starline/translations/pl.json @@ -30,8 +30,8 @@ }, "auth_user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Adres e-mail i has\u0142o do konta StarLine", "title": "Po\u015bwiadczenia u\u017cytkownika" diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index ce16b10f548..d8c32575b17 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -1,7 +1,7 @@ """Support for stiebel_eltron climate platform.""" import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([StiebelEltron(name, ste_data)], True) -class StiebelEltron(ClimateDevice): +class StiebelEltron(ClimateEntity): """Representation of a STIEBEL ELTRON heat pump.""" def __init__(self, name, ste_data): diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index c8515f401da..a07f208ac9d 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -5,7 +5,7 @@ import logging import stookalert import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers import config_validation as cv @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([StookalertBinarySensor(name, api_handler)], update_before_add=True) -class StookalertBinarySensor(BinarySensorDevice): +class StookalertBinarySensor(BinarySensorEntity): """An implementation of RIVM Stookalert.""" def __init__(self, name, api_handler): diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 873d76cf189..2cc60938a8d 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["av==6.1.2"], + "requirements": ["av==7.0.1"], "dependencies": ["http"], "codeowners": ["@hunterjm"], "quality_scale": "internal" diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 1dd90b8b804..c28c73c64ac 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -17,7 +17,8 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: List[Segment]): """Handle saving stream.""" - output = av.open(file_out, "w", options={"movflags": "frag_keyframe"}) + first_pts = None + output = av.open(file_out, "w") output_v = None for segment in segments: @@ -29,13 +30,22 @@ def recorder_save_worker(file_out: str, segments: List[Segment]): # Add output streams if not output_v: output_v = output.add_stream(template=source_v) + context = output_v.codec_context + context.flags |= "GLOBAL_HEADER" # Remux video for packet in source.demux(source_v): if packet is not None and packet.dts is not None: + if first_pts is None: + first_pts = packet.pts + + packet.pts -= first_pts + packet.dts -= first_pts packet.stream = output_v output.mux(packet) + source.close() + output.close() diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 836bc9b4183..336e92358ee 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -59,7 +59,9 @@ def setup(hass, config): "Streamlabs Water Monitor auto-detected location_id=%s", location_id ) else: - location = next((l for l in locations if location_id == l["locationId"]), None) + location = next( + (loc for loc in locations if location_id == loc["locationId"]), None + ) if location is None: _LOGGER.error("Supplied location_id is invalid") return False diff --git a/homeassistant/components/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index 78b2ceb4044..a25f5e124e6 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.streamlabswater import DOMAIN as STREAMLABSWATER_DOMAIN from homeassistant.util import Throttle @@ -46,7 +46,7 @@ class StreamlabsLocationData: return self._is_away -class StreamlabsAwayMode(BinarySensorDevice): +class StreamlabsAwayMode(BinarySensorEntity): """Monitor the away mode state.""" def __init__(self, location_name, streamlabs_location_data): diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 659b78cc41a..1c0f2f60431 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -5,7 +5,7 @@ from pprint import pformat from homeassistant.components.cover import ( ATTR_POSITION, DEVICE_CLASS_GARAGE, - CoverDevice, + CoverEntity, ) from homeassistant.components.supla import SuplaChannel @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities) -class SuplaCover(SuplaChannel, CoverDevice): +class SuplaCover(SuplaChannel, CoverEntity): """Representation of a Supla Cover.""" @property @@ -67,7 +67,7 @@ class SuplaCover(SuplaChannel, CoverDevice): self.action("STOP") -class SuplaGateDoor(SuplaChannel, CoverDevice): +class SuplaGateDoor(SuplaChannel, CoverEntity): """Representation of a Supla gate door.""" @property diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 556c1b69a53..61f218b75d9 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -3,7 +3,7 @@ import logging from pprint import pformat from homeassistant.components.supla import SuplaChannel -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SuplaSwitch(device) for device in discovery_info]) -class SuplaSwitch(SuplaChannel, SwitchDevice): +class SuplaSwitch(SuplaChannel, SwitchEntity): """Representation of a Supla Switch.""" def turn_on(self, **kwargs): diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index a22ba4a1335..90e754118ab 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -32,6 +32,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, SPC, + SURE_API_TIMEOUT, TOPIC_UPDATE, ) @@ -78,6 +79,7 @@ async def async_setup(hass, config) -> bool: conf[CONF_PASSWORD], hass.loop, async_get_clientsession(hass), + api_timeout=SURE_API_TIMEOUT, ) await surepy.get_data() except SurePetcareAuthenticationError: diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 5b3ac492137..efd5048053f 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -8,7 +8,7 @@ from surepy import SureLocationID, SureProductID from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PRESENCE, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import CONF_ID, CONF_TYPE from homeassistant.core import callback @@ -55,7 +55,7 @@ async def async_setup_platform( async_add_entities(entities, True) -class SurePetcareBinarySensor(BinarySensorDevice): +class SurePetcareBinarySensor(BinarySensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" def __init__( @@ -105,7 +105,7 @@ class SurePetcareBinarySensor(BinarySensorDevice): return None if not self._device_class else self._device_class @property - def unique_id(self: BinarySensorDevice) -> str: + def unique_id(self) -> str: """Return an unique ID.""" return f"{self._spc_data['household_id']}-{self._id}" @@ -214,7 +214,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): return f"{self._name}_connectivity" @property - def unique_id(self: BinarySensorDevice) -> str: + def unique_id(self) -> str: """Return an unique ID.""" return f"{self._spc_data['household_id']}-{self._id}-connectivity" diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index d534398784f..7f0213be4ef 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -23,6 +23,9 @@ SURE_IDS = "sure_ids" # platforms TOPIC_UPDATE = f"{DOMAIN}_data_update" +# sure petcare api +SURE_API_TIMEOUT = 15 + # flap BATTERY_ICON = "mdi:battery" SURE_BATT_VOLTAGE_FULL = 1.6 # voltage diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 6d34ff477ce..659a6091299 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,5 +3,5 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.2.3"] + "requirements": ["surepy==0.2.5"] } diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 3884b90c464..1d9b54a0424 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -78,7 +78,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class SwitchDevice(ToggleEntity): +class SwitchEntity(ToggleEntity): """Representation of a switch.""" @property @@ -112,3 +112,15 @@ class SwitchDevice(ToggleEntity): def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" return None + + +class SwitchDevice(SwitchEntity): + """Representation of a switch (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "SwitchDevice is deprecated, modify %s to extend SwitchEntity", + cls.__name__, + ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 92b64b36b93..f40ccde5b0b 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -5,7 +5,7 @@ from typing import Callable, Optional, Sequence, cast import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.light import PLATFORM_SCHEMA, Light +from homeassistant.components.light import PLATFORM_SCHEMA, LightEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -49,7 +49,7 @@ async def async_setup_platform( ) -class LightSwitch(Light): +class LightSwitch(LightEntity): """Represents a Switch as a Light.""" def __init__(self, name: str, switch_entity_id: str) -> None: diff --git a/homeassistant/components/switch/translations/es-419.json b/homeassistant/components/switch/translations/es-419.json index 83dc31ade83..7fb04127b15 100644 --- a/homeassistant/components/switch/translations/es-419.json +++ b/homeassistant/components/switch/translations/es-419.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, diff --git a/homeassistant/components/switch/translations/no.json b/homeassistant/components/switch/translations/no.json index dc57fa94203..a8ed6128773 100644 --- a/homeassistant/components/switch/translations/no.json +++ b/homeassistant/components/switch/translations/no.json @@ -14,5 +14,11 @@ "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } }, + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, "title": "Bryter" } \ No newline at end of file diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index f0cbecc8968..6d32f8cfd10 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -6,7 +6,7 @@ from typing import Any, Dict import switchbot 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_MAC, CONF_NAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SwitchBot(mac_addr, name, password)]) -class SwitchBot(SwitchDevice, RestoreEntity): +class SwitchBot(SwitchEntity, RestoreEntity): """Representation of a Switchbot.""" def __init__(self, mac, name, password) -> None: diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 0545687b003..8369fdd8975 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -7,11 +7,12 @@ from typing import Dict, Optional from aioswitcher.api import SwitcherV2Api from aioswitcher.bridge import SwitcherV2Bridge +from aioswitcher.consts import COMMAND_ON import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_EDIT from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import CONF_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback, split_entity_id from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv @@ -32,6 +33,7 @@ _LOGGER = getLogger(__name__) DOMAIN = "switcher_kis" CONF_AUTO_OFF = "auto_off" +CONF_TIMER_MINUTES = "timer_minutes" CONF_DEVICE_ID = "device_id" CONF_DEVICE_PASSWORD = "device_password" CONF_PHONE_ID = "phone_id" @@ -60,11 +62,21 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" SERVICE_SET_AUTO_OFF_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ENTITY_ID): cv.entity_id, vol.Required(CONF_AUTO_OFF): cv.time_period_str, } ) +SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" +SERVICE_TURN_ON_WITH_TIMER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TIMER_MINUTES): vol.All( + cv.positive_int, vol.Range(min=1, max=90) + ), + } +) + @bind_hass async def _validate_edit_permission( @@ -76,13 +88,11 @@ async def _validate_edit_permission( raise Unauthorized( context=context, entity_id=entity_id, permission=(POLICY_EDIT,) ) - user = await hass.auth.async_get_user(context.user_id) if user is None: raise UnknownUser( context=context, entity_id=entity_id, permission=(POLICY_EDIT,) ) - if not user.permissions.check_entity(entity_id, POLICY_EDIT): raise Unauthorized( context=context, entity_id=entity_id, permission=(POLICY_EDIT,) @@ -112,7 +122,6 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: _LOGGER.exception("Failed to get response from device") await v2bridge.stop() return False - hass.data[DOMAIN] = {DATA_DEVICE: device_data} async def async_switch_platform_discovered( @@ -126,7 +135,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Use for handling setting device auto-off service calls.""" await _validate_edit_permission( - hass, service.context, service.data[CONF_ENTITY_ID] + hass, service.context, service.data[ATTR_ENTITY_ID] ) async with SwitcherV2Api( @@ -134,6 +143,18 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: ) as swapi: await swapi.set_auto_shutdown(service.data[CONF_AUTO_OFF]) + async def async_turn_on_with_timer_service(service: ServiceCallType) -> None: + """Use for handling turning device on with a timer service calls.""" + + await _validate_edit_permission( + hass, service.context, service.data[ATTR_ENTITY_ID] + ) + + async with SwitcherV2Api( + hass.loop, device_data.ip_addr, phone_id, device_id, device_password + ) as swapi: + await swapi.control_device(COMMAND_ON, service.data[CONF_TIMER_MINUTES]) + hass.services.async_register( DOMAIN, SERVICE_SET_AUTO_OFF_NAME, @@ -141,6 +162,13 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: schema=SERVICE_SET_AUTO_OFF_SCHEMA, ) + hass.services.async_register( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + async_turn_on_with_timer_service, + schema=SERVICE_TURN_ON_WITH_TIMER_SCHEMA, + ) + async_listen_platform(hass, SWITCH_DOMAIN, async_switch_platform_discovered) hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config)) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index bc608276897..a4e312908f2 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,5 +3,5 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi"], - "requirements": ["aioswitcher==1.1.0"] + "requirements": ["aioswitcher==1.2.0"] } diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index 39691752445..07e0cfe1198 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -7,3 +7,13 @@ set_auto_off: auto_off: description: "Time period string containing hours and minutes." example: '"02:30"' + +turn_on_with_timer: + description: 'Turn on the Switcher device with timer.' + fields: + entity_id: + description: "Name of the entity id associated with the integration, used for permission validation." + example: "switch.switcher_kis_boiler" + timer_minutes: + description: 'Minutes to turn on (valid range from 1 to 90)' + example: '30' diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index ea32183b511..c2254968901 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -12,7 +12,7 @@ from aioswitcher.consts import ( WAITING_TEXT, ) -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType @@ -53,7 +53,7 @@ async def async_setup_platform( async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])]) -class SwitcherControl(SwitchDevice): +class SwitcherControl(SwitchEntity): """Home Assistant switch entity.""" def __init__(self, device_data: "SwitcherV2Device") -> None: diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 268115434cf..e09e42fe75b 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -6,7 +6,7 @@ import logging import switchmate 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_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -34,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None) -> None: add_entities([SwitchmateEntity(mac_addr, name, flip_on_off)], True) -class SwitchmateEntity(SwitchDevice): +class SwitchmateEntity(SwitchEntity): """Representation of a Switchmate.""" def __init__(self, mac, name, flip_on_off) -> None: diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 9431cb7b1c9..b2ff2d2e8ef 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_MAC, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, ) @@ -22,7 +23,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_VOLUMES, DEFAULT_SSL, DOMAIN +from .const import ( + CONF_VOLUMES, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SSL, + DOMAIN, + SYNO_API, + UNDO_UPDATE_LISTENER, +) CONFIG_SCHEMA = vol.Schema( { @@ -41,8 +49,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCAN_INTERVAL = timedelta(minutes=15) - async def async_setup(hass, config): """Set up Synology DSM sensors from legacy config file.""" @@ -63,20 +69,17 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Synology DSM sensors.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - unit = hass.config.units.temperature_unit - use_ssl = entry.data[CONF_SSL] - device_token = entry.data.get("device_token") - - api = SynoApi(hass, host, port, username, password, unit, use_ssl, device_token) + api = SynoApi(hass, entry) await api.async_setup() + undo_listener = entry.add_update_listener(_async_update_listener) + hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = api + hass.data[DOMAIN][entry.unique_id] = { + SYNO_API: api, + UNDO_UPDATE_LISTENER: undo_listener, + } # For SSDP compat if not entry.data.get(CONF_MAC): @@ -94,34 +97,29 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Synology DSM sensors.""" - api = hass.data[DOMAIN][entry.unique_id] - await api.async_unload() - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor") + + if unload_ok: + entry_data = hass.data[DOMAIN][entry.unique_id] + entry_data[UNDO_UPDATE_LISTENER]() + await entry_data[SYNO_API].async_unload() + hass.data[DOMAIN].pop(entry.unique_id) + + return unload_ok + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) class SynoApi: """Class to interface with Synology DSM API.""" - def __init__( - self, - hass: HomeAssistantType, - host: str, - port: int, - username: str, - password: str, - temp_unit: str, - use_ssl: bool, - device_token: str, - ): + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): """Initialize the API wrapper class.""" self._hass = hass - self._host = host - self._port = port - self._username = username - self._password = password - self._use_ssl = use_ssl - self._device_token = device_token - self.temp_unit = temp_unit + self._entry = entry self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None @@ -138,19 +136,25 @@ class SynoApi: async def async_setup(self): """Start interacting with the NAS.""" self.dsm = SynologyDSM( - self._host, - self._port, - self._username, - self._password, - self._use_ssl, - device_token=self._device_token, + self._entry.data[CONF_HOST], + self._entry.data[CONF_PORT], + self._entry.data[CONF_USERNAME], + self._entry.data[CONF_PASSWORD], + self._entry.data[CONF_SSL], + device_token=self._entry.data.get("device_token"), ) await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.update() self._unsub_dispatcher = async_track_time_interval( - self._hass, self.update, SCAN_INTERVAL + self._hass, + self.update, + timedelta( + minutes=self._entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + ), ) def _fetch_device_configuration(self): diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 5d8e43ab175..a4d7d75e073 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -21,11 +21,20 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv -from .const import CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SSL +from .const import ( + CONF_VOLUMES, + DEFAULT_PORT, + DEFAULT_PORT_SSL, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SSL, +) from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -61,6 +70,12 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return SynologyDSMOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the synology_dsm config flow.""" self.saved_user_input = {} @@ -216,6 +231,31 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return mac in existing_macs +class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): cv.positive_int + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + def _login_and_fetch_syno_info(api, otp_code): """Login to the NAS and fetch basic data.""" # These do i/o diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index b3c9f66c8da..e0a166e908b 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -9,10 +9,17 @@ from homeassistant.const import ( DOMAIN = "synology_dsm" BASE_NAME = "Synology" +# Entry keys +SYNO_API = "syno_api" +UNDO_UPDATE_LISTENER = "undo_update_listener" + +# Configuration CONF_VOLUMES = "volumes" DEFAULT_SSL = True DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 +# Options +DEFAULT_SCAN_INTERVAL = 15 # min UTILISATION_SENSORS = { "cpu_other_load": ["CPU Load (Other)", UNIT_PERCENTAGE, "mdi:chip"], diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index f57f1843f45..fcf91bb25b3 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["python-synology==0.8.0"], + "requirements": ["python-synology==0.8.1"], "codeowners": ["@ProtoThis", "@Quentame"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 8b5da35177e..81873cad4cd 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -8,12 +8,13 @@ from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, DATA_TERABYTES, + PRECISION_TENTHS, TEMP_CELSIUS, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util.temperature import celsius_to_fahrenheit from . import SynoApi from .const import ( @@ -22,6 +23,7 @@ from .const import ( DOMAIN, STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, + SYNO_API, TEMP_SENSORS_KEYS, UTILISATION_SENSORS, ) @@ -34,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up the Synology NAS Sensor.""" - api = hass.data[DOMAIN][entry.unique_id] + api = hass.data[DOMAIN][entry.unique_id][SYNO_API] sensors = [ SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) @@ -61,7 +63,7 @@ async def async_setup_entry( for sensor_type in STORAGE_DISK_SENSORS ] - async_add_entities(sensors, True) + async_add_entities(sensors) class SynoNasSensor(Entity): @@ -108,7 +110,7 @@ class SynoNasSensor(Entity): def unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" if self.sensor_type in TEMP_SENSORS_KEYS: - return self._api.temp_unit + return self.hass.config.units.temperature_unit return self._unit @property @@ -132,6 +134,10 @@ class SynoNasSensor(Entity): """No polling needed.""" return False + async def async_update(self): + """Only used by the generic entity update service.""" + await self._api.update() + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( @@ -181,12 +187,8 @@ class SynoNasStorageSensor(SynoNasSensor): return round(attr / 1024.0 ** 4, 2) # Temperature - if self._api.temp_unit == TEMP_CELSIUS: - # Celsius - return attr if self.sensor_type in TEMP_SENSORS_KEYS: - # Fahrenheit - return celsius_to_fahrenheit(attr) + return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS) return attr diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index c58b0d819ea..0024a7db612 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -5,11 +5,11 @@ "user": { "title": "Synology DSM", "data": { - "host": "Host", - "port": "Port (Optional)", + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", "ssl": "Use SSL/TLS to connect to your NAS", - "username": "Username", - "password": "Password" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, "2sa": { @@ -23,9 +23,9 @@ "description": "Do you want to setup {name} ({host})?", "data": { "ssl": "Use SSL/TLS to connect to your NAS", - "username": "Username", - "password": "Password", - "port": "Port (Optional)" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -36,6 +36,17 @@ "otp_failed": "Two-step authentication failed, retry with a new pass code", "unknown": "Unknown error: please check logs to get more details" }, - "abort": { "already_configured": "Host already configured" } + "abort": { + "already_configured": "Host already configured" + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutes between scans" + } + } + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index f593ac26182..26f5d0c76b7 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "connection": "Error de connexi\u00f3: comprova l'amfitri\u00f3, la contrasenya i l'SSL", - "login": "Error d\u2019inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya", + "login": "Error d'inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya", "missing_data": "Falten dades: torna-ho a provar m\u00e9s tard o prova una altra configuraci\u00f3 diferent", "otp_failed": "L'autenticaci\u00f3 en dos passos ha fallat, torna-ho a provar amb un nou codi", "unknown": "Error desconegut: consulta els registres per a m\u00e9s detalls." @@ -41,5 +41,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minuts entre escanejos" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 625bec2c5ae..54ee7a9b623 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -41,5 +41,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minuten zwischen den Scans" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 50860f96639..ed3dae27151 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -20,8 +20,9 @@ }, "link": { "data": { + "api_version": "DSM version", "password": "Password", - "port": "Port (Optional)", + "port": "Port", "ssl": "Use SSL/TLS to connect to your NAS", "username": "Username" }, @@ -30,14 +31,24 @@ }, "user": { "data": { + "api_version": "DSM version", "host": "Host", "password": "Password", - "port": "Port (Optional)", + "port": "Port", "ssl": "Use SSL/TLS to connect to your NAS", "username": "Username" }, "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutes between scans" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/es-419.json b/homeassistant/components/synology_dsm/translations/es-419.json new file mode 100644 index 00000000000..cad627e0dfb --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/es-419.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Host ya configurado" + }, + "error": { + "connection": "Error de conexi\u00f3n: compruebe su host, puerto y ssl", + "login": "Error de inicio de sesi\u00f3n: compruebe su nombre de usuario y contrase\u00f1a", + "missing_data": "Datos faltantes: vuelva a intentarlo m\u00e1s tarde u otra configuraci\u00f3n", + "otp_failed": "La autenticaci\u00f3n de dos pasos fall\u00f3, vuelva a intentar con un nuevo c\u00f3digo de acceso", + "unknown": "Error desconocido: verifique los registros para obtener m\u00e1s detalles" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "2sa": { + "data": { + "otp_code": "C\u00f3digo" + }, + "title": "Synology DSM: autenticaci\u00f3n en dos pasos" + }, + "link": { + "data": { + "api_version": "Versi\u00f3n DSM", + "password": "Contrase\u00f1a", + "port": "Puerto (opcional)", + "ssl": "Utilice SSL/TLS para conectarse a su NAS", + "username": "Nombre de usuario" + }, + "description": "\u00bfDesea configurar {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "api_version": "Versi\u00f3n DSM", + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto (opcional)", + "ssl": "Utilice SSL/TLS para conectarse a su NAS", + "username": "Nombre de usuario" + }, + "title": "Synology DSM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 122f8bef51d..6efe92020d2 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -34,12 +34,21 @@ "api_version": "Versi\u00f3n del DSM", "host": "Host", "password": "Contrase\u00f1a", - "port": "Puerto", + "port": "Puerto (opcional)", "ssl": "Usar SSL/TLS para conectar con tu NAS", "username": "Usuario" }, "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutos entre escaneos" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/fi.json b/homeassistant/components/synology_dsm/translations/fi.json new file mode 100644 index 00000000000..4f5f2cc19fa --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/fi.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "otp_failed": "Kaksivaiheinen todennus ep\u00e4onnistui, yrit\u00e4 uudelleen uudella salasanalla." + }, + "step": { + "2sa": { + "data": { + "otp_code": "Koodi" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minuutit skannausten v\u00e4lill\u00e4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json new file mode 100644 index 00000000000..98b3a2214d7 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/he.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u05d3\u05e7\u05d5\u05ea \u05d1\u05d9\u05df \u05e1\u05e8\u05d9\u05e7\u05d5\u05ea." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json new file mode 100644 index 00000000000..0bb810d66e2 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "link": { + "data": { + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index e7ce33ec43c..3bdd7ab0faf 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -4,9 +4,11 @@ "already_configured": "Host gi\u00e0 configurato" }, "error": { + "connection": "Errore di connessione: controlla host, porta e SSL", "login": "Errore di accesso: si prega di controllare il nome utente e la password", "missing_data": "Dati mancanti: si prega di riprovare pi\u00f9 tardi o un'altra configurazione", - "otp_failed": "Autenticazione in due fasi fallita, riprovare con un nuovo codice di accesso" + "otp_failed": "Autenticazione in due fasi fallita, riprovare con un nuovo codice di accesso", + "unknown": "Errore sconosciuto: si prega di controllare i registri per ottenere maggiori dettagli" }, "flow_title": "Synology DSM {name} ({host})", "step": { @@ -39,5 +41,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minuti tra una scansione e l'altra" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index d2085b7a097..252c68f04ad 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -4,15 +4,25 @@ "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc0ac\uc6a9\uc790 \uc774\ub984 \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" + "connection": "\ub85c\uadf8\uc778 \uc624\ub958: \ud638\uc2a4\ud2b8\ub098 \ud3ec\ud2b8 \ub610\ub294 \uc778\uc99d\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc0ac\uc6a9\uc790 \uc774\ub984 \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "missing_data": "\ub204\ub77d\ub41c \ub370\uc774\ud130: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uac70\ub098 \ub2e4\ub978 \uad6c\uc131\uc744 \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694", + "otp_failed": "2\ub2e8\uacc4 \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \ud328\uc2a4 \ucf54\ub4dc\ub85c \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694" }, "flow_title": "Synology DSM {name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "\ucf54\ub4dc" + }, + "title": "Synology DSM: 2\ub2e8\uacc4 \uc778\uc99d" + }, "link": { "data": { "api_version": "DSM \ubc84\uc804", "password": "\ube44\ubc00\ubc88\ud638", - "port": "\ud3ec\ud2b8 (\uc120\ud0dd \uc0ac\ud56d)", + "port": "\ud3ec\ud2b8", "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, @@ -31,5 +41,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/lb.json b/homeassistant/components/synology_dsm/translations/lb.json index 5453b078a3e..42bbc08b6cf 100644 --- a/homeassistant/components/synology_dsm/translations/lb.json +++ b/homeassistant/components/synology_dsm/translations/lb.json @@ -41,5 +41,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutte t\u00ebscht Scannen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index bda3e337dda..8b69639140b 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -3,12 +3,20 @@ "abort": { "already_configured": "Host is al geconfigureerd." }, + "error": { + "connection": "Verbindingsfout: controleer uw host, poort & ssl", + "login": "Aanmeldingsfout: controleer uw gebruikersnaam en wachtwoord", + "missing_data": "Ontbrekende gegevens: probeer het later opnieuw of een andere configuratie", + "otp_failed": "Tweestapsverificatie is mislukt, probeer het opnieuw met een nieuwe toegangscode", + "unknown": "Onbekende fout: controleer de logs voor meer informatie" + }, "flow_title": "Synology DSM {name} ({host})", "step": { "2sa": { "data": { "otp_code": "Code" - } + }, + "title": "Synology DSM: tweestapsverificatie" }, "link": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index d9e24638f72..40d7907d61d 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -4,11 +4,11 @@ "already_configured": "Verten er allerede konfigurert" }, "error": { - "connection": "Tilkoblingsfeil: sjekk verten, porten og ssl", + "connection": "Tilkoblingsfeil: Vennligst sjekk verten, porten og SSL", "login": "P\u00e5loggingsfeil: Vennligst sjekk brukernavnet ditt og passordet ditt", - "missing_data": "Manglende data: Pr\u00f8v p\u00e5 nytt senere eller en annen konfigurasjon", - "otp_failed": "To-trinns autentisering mislyktes. Pr\u00f8v p\u00e5 nytt med en ny passkode", - "unknown": "Ukjent feil: sjekk loggene for \u00e5 f\u00e5 flere detaljer" + "missing_data": "Manglende data: Vennligst pr\u00f8v p\u00e5 nytt senere eller en annen konfigurasjon", + "otp_failed": "Totrinnsgodkjenning mislyktes, pr\u00f8v p\u00e5 nytt med en ny passord", + "unknown": "Ukjent feil: Vennligst sjekk loggene for \u00e5 f\u00e5 flere detaljer" }, "flow_title": "Synology DSM {name} ({host})", "step": { @@ -27,7 +27,7 @@ "username": "Brukernavn" }, "description": "Vil du konfigurere {name} ({host})?", - "title": "Synology DSM" + "title": "" }, "user": { "data": { @@ -38,7 +38,16 @@ "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en", "username": "Brukernavn" }, - "title": "Synology DSM" + "title": "" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutter mellom skanninger" + } } } } diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index d7714de27eb..33644773220 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -8,7 +8,7 @@ "login": "B\u0142\u0105d logowania: sprawd\u017a nazw\u0119 u\u017cytkownika i has\u0142o", "missing_data": "Brakuj\u0105ce dane: spr\u00f3buj ponownie p\u00f3\u017aniej lub skorzystaj z innej konfiguracji", "otp_failed": "Uwierzytelnianie dwuetapowe nie powiod\u0142o si\u0119, spr\u00f3buj ponownie z nowym kodem dost\u0119pu", - "unknown": "Nieznany b\u0142\u0105d, sprawd\u017a logi aby uzyska\u0107 wi\u0119cej szczeg\u00f3\u0142\u00f3w" + "unknown": "Nieznany b\u0142\u0105d, sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej szczeg\u00f3\u0142\u00f3w" }, "flow_title": "Synology DSM {name} ({host})", "step": { @@ -21,10 +21,10 @@ "link": { "data": { "api_version": "Wersja DSM", - "password": "Has\u0142o", - "port": "Port (opcjonalnie)", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%] (opcjonalnie)", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z serwerem NAS", - "username": "Nazwa u\u017cytkownika" + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", "title": "Synology DSM" @@ -32,14 +32,23 @@ "user": { "data": { "api_version": "Wersja DSM", - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "port": "Port (opcjonalnie)", + "host": "[%key_id:common::config_flow::data::host%]", + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%] (opcjonalnie)", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z serwerem NAS", - "username": "Nazwa u\u017cytkownika" + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji [min]" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index d136f171397..9d1ca6ca39f 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -22,7 +22,7 @@ "data": { "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f DSM", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "port": "\u041f\u043e\u0440\u0442 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "username": "\u041b\u043e\u0433\u0438\u043d" }, @@ -34,12 +34,21 @@ "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f DSM", "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "port": "\u041f\u043e\u0440\u0442 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "username": "\u041b\u043e\u0433\u0438\u043d" }, "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u043c\u0438\u043d.)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/sl.json b/homeassistant/components/synology_dsm/translations/sl.json index 168454a4331..1ad2e70a148 100644 --- a/homeassistant/components/synology_dsm/translations/sl.json +++ b/homeassistant/components/synology_dsm/translations/sl.json @@ -4,10 +4,20 @@ "already_configured": "Gostitelj je \u017ee konfiguriran" }, "error": { - "login": "Napaka pri prijavi: preverite svoje uporabni\u0161ko ime in geslo" + "connection": "Napaka pri povezavi: preverite gostitelja, vrata in ssl", + "login": "Napaka pri prijavi: preverite svoje uporabni\u0161ko ime in geslo", + "missing_data": "Manjkajo\u010di podatki: poskusite pozneje ali v drugi konfiguraciji", + "otp_failed": "Dvostopenjska avtentikacija ni uspela. Poskusite z novim geslom", + "unknown": "Neznana napaka: za ve\u010d podrobnosti preverite dnevnike" }, "flow_title": "Synology DSM {name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "Koda" + }, + "title": "Synology DSM: dvostopenjska avtentikacija" + }, "link": { "data": { "api_version": "Razli\u010dica DSM", diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json new file mode 100644 index 00000000000..6aaee8b44aa --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "connection": "Anslutningsfel: v\u00e4nligen kontrollera v\u00e4rd, port & SSL" + }, + "step": { + "link": { + "data": { + "password": "L\u00f6senord", + "port": "Port (Valfri)", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Do vill du konfigurera {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minuter mellan skanningar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 4e2173324c3..07d469bedd2 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -22,7 +22,7 @@ "data": { "api_version": "DSM \u7248\u672c", "password": "\u5bc6\u78bc", - "port": "\u901a\u8a0a\u57e0\uff08\u9078\u9805\uff09", + "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 NAS", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, @@ -34,12 +34,21 @@ "api_version": "DSM \u7248\u672c", "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc", - "port": "\u901a\u8a0a\u57e0\uff08\u9078\u9805\uff09", + "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 NAS", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "title": "\u7fa4\u6689 DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u9694\u5206\u6578" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 5e1bfa94dec..cdcc32f7ac2 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,7 +1,7 @@ """Support for Tado thermostats.""" import logging -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_OFF, FAN_AUTO, @@ -163,7 +163,7 @@ def create_climate_entity(tado, name: str, zone_id: int, zone: dict): return entity -class TadoClimate(TadoZoneEntity, ClimateDevice): +class TadoClimate(TadoZoneEntity, ClimateEntity): """Representation of a Tado climate entity.""" def __init__( diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index c14b4284cf3..fb60b820ab9 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -92,6 +92,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # they already have one configured as they can always # add a new one via "+" return self.async_abort(reason="already_configured") + properties = { + key.lower(): value for (key, value) in homekit_info["properties"].items() + } + await self.async_set_unique_id(properties["id"]) return await self.async_step_user() async def async_step_import(self, user_input): diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index 1192ba544d9..f0f2ce4ab99 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -1,9 +1,14 @@ { "config": { - "abort": { "already_configured": "Device is already configured" }, + "abort": { + "already_configured": "Device is already configured" + }, "step": { "user": { - "data": { "password": "Password", "username": "Username" }, + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, "title": "Connect to your Tado account" } }, @@ -18,9 +23,11 @@ "step": { "init": { "description": "Fallback mode will switch to Smart Schedule at next schedule switch after manually adjusting a zone.", - "data": { "fallback": "Enable fallback mode." }, + "data": { + "fallback": "Enable fallback mode." + }, "title": "Adjust Tado options." } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/es-419.json b/homeassistant/components/tado/translations/es-419.json new file mode 100644 index 00000000000..3b9f1381eda --- /dev/null +++ b/homeassistant/components/tado/translations/es-419.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "no_homes": "No hay hogares vinculados a esta cuenta Tado.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a su cuenta de Tado" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Habilitar el modo de fallback." + }, + "description": "El modo Fallback cambiar\u00e1 a Smart Schedule en el siguiente cambio de programaci\u00f3n despu\u00e9s de ajustar manualmente una zona.", + "title": "Ajusta las opciones de Tado." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json index 3fa024bb95e..18196a4bf13 100644 --- a/homeassistant/components/tado/translations/fr.json +++ b/homeassistant/components/tado/translations/fr.json @@ -5,14 +5,27 @@ }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "invalid_auth": "Authentification non valide" + "invalid_auth": "Authentification non valide", + "no_homes": "Il n\u2019y a pas de maisons li\u00e9es \u00e0 ce compte tado.", + "unknown": "Erreur inattendue" }, "step": { "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Connectez-vous \u00e0 votre compte Tado" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Activer le mode restreint." + }, + "title": "Ajustez les options de Tado." } } } diff --git a/homeassistant/components/tado/translations/ko.json b/homeassistant/components/tado/translations/ko.json index 08561ee43aa..8982b68829b 100644 --- a/homeassistant/components/tado/translations/ko.json +++ b/homeassistant/components/tado/translations/ko.json @@ -26,7 +26,7 @@ "fallback": "\ub300\uccb4 \ubaa8\ub4dc\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4." }, "description": "\uc601\uc5ed\uc744 \uc218\ub3d9\uc73c\ub85c \uc804\ud658\ud558\uba74 \ub300\uccb4 \ubaa8\ub4dc\ub294 \ub2e4\uc74c \uc77c\uc815\uc744 \uc2a4\ub9c8\ud2b8 \uc77c\uc815\uc73c\ub85c \uc804\ud658\ud569\ub2c8\ub2e4.", - "title": "Tado \uc635\uc158 \uc870\uc815." + "title": "Tado \uc635\uc158 \uc870\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/tado/translations/nl.json b/homeassistant/components/tado/translations/nl.json new file mode 100644 index 00000000000..3cdadf0f54e --- /dev/null +++ b/homeassistant/components/tado/translations/nl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "no_homes": "Er zijn geen huizen gekoppeld aan dit tado-account.", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Maak verbinding met je Tado-account" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Schakel de terugvalmodus in." + }, + "description": "De fallback-modus schakelt over naar Smart Schedule bij de volgende schemaschakeling na het handmatig aanpassen van een zone.", + "title": "Pas Tado-opties aan." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/pl.json b/homeassistant/components/tado/translations/pl.json index 65a23d58359..afe884ced51 100644 --- a/homeassistant/components/tado/translations/pl.json +++ b/homeassistant/components/tado/translations/pl.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", "no_homes": "Brak dom\u00f3w powi\u0105zanych z tym kontem Tado.", - "unknown": "Niespodziewany b\u0142\u0105d." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Po\u0142\u0105cz z kontem Tado" } @@ -22,6 +22,10 @@ "options": { "step": { "init": { + "data": { + "fallback": "W\u0142\u0105cz tryb awaryjny." + }, + "description": "Tryb rezerwowy prze\u0142\u0105czy si\u0119 na inteligentny harmonogram przy nast\u0119pnym prze\u0142\u0105czeniu z harmonogramu po r\u0119cznym dostosowaniu strefy.", "title": "Dostosuj opcje Tado" } } diff --git a/homeassistant/components/tado/translations/sv.json b/homeassistant/components/tado/translations/sv.json new file mode 100644 index 00000000000..90be3d809c9 --- /dev/null +++ b/homeassistant/components/tado/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "no_homes": "Det finns inga hem kopplade till detta tado-konto.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "title": "Anslut till ditt Tado-konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Aktivera reservl\u00e4ge." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index aeb2d1ee106..1c0d37c90df 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -98,7 +98,7 @@ def create_water_heater_entity(tado, name: str, zone_id: int, zone: str): return entity -class TadoWaterHeater(TadoZoneEntity, WaterHeaterDevice): +class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Representation of a Tado water heater.""" def __init__( diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index 7621a542838..39e492601bd 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -24,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class TahomaBinarySensor(TahomaDevice, BinarySensorDevice): +class TahomaBinarySensor(TahomaDevice, BinarySensorEntity): """Representation of a Tahoma Binary Sensor.""" def __init__(self, tahoma_device, controller): diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index e13f9bb8859..2eec9160811 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, - CoverDevice, + CoverEntity, ) from homeassistant.util.dt import utcnow @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class TahomaCover(TahomaDevice, CoverDevice): +class TahomaCover(TahomaDevice, CoverEntity): """Representation a Tahoma Cover.""" def __init__(self, tahoma_device, controller): diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py index 0b02975fc7e..93d82bffc99 100644 --- a/homeassistant/components/tahoma/lock.py +++ b/homeassistant/components/tahoma/lock.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -24,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class TahomaLock(TahomaDevice, LockDevice): +class TahomaLock(TahomaDevice, LockEntity): """Representation a Tahoma lock.""" def __init__(self, tahoma_device, controller): diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index 9f98e711ac9..13aa70c66d3 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -1,7 +1,7 @@ """Support for Tahoma switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -22,7 +22,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class TahomaSwitch(TahomaDevice, SwitchDevice): +class TahomaSwitch(TahomaDevice, SwitchEntity): """Representation a Tahoma Switch.""" def __init__(self, tahoma_device, controller): diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index e54bc7298b0..912dd27c887 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -5,7 +5,7 @@ import logging from tapsaff import TapsAff import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([TapsAffSensor(taps_aff_data, name)], True) -class TapsAffSensor(BinarySensorDevice): +class TapsAffSensor(BinarySensorEntity): """Implementation of a Taps Aff binary sensor.""" def __init__(self, taps_aff_data, name): diff --git a/homeassistant/components/tcp/binary_sensor.py b/homeassistant/components/tcp/binary_sensor.py index 4d26d819ede..1648ab5e247 100644 --- a/homeassistant/components/tcp/binary_sensor.py +++ b/homeassistant/components/tcp/binary_sensor.py @@ -1,7 +1,7 @@ """Provides a binary sensor which gets its values from a TCP socket.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from .sensor import CONF_VALUE_ON, PLATFORM_SCHEMA, TcpSensor @@ -15,7 +15,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([TcpBinarySensor(hass, config)]) -class TcpBinarySensor(BinarySensorDevice, TcpSensor): +class TcpBinarySensor(BinarySensorEntity, TcpSensor): """A binary sensor which is on when its state == CONF_VALUE_ON.""" required = (CONF_VALUE_ON,) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 16da2e741e4..7c8f976a049 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -11,6 +11,7 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, ) +from homeassistant.helpers.network import get_url from . import ( CONF_ALLOWED_CHAT_IDS, @@ -32,7 +33,9 @@ async def async_setup_platform(hass, config): bot = initialize_bot(config) current_status = await hass.async_add_job(bot.getWebhookInfo) - base_url = config.get(CONF_URL, hass.config.api.base_url) + base_url = config.get( + CONF_URL, get_url(hass, require_ssl=True, allow_internal=False) + ) # Some logging of Bot current status: last_error_date = getattr(current_status, "last_error_date", None) diff --git a/homeassistant/components/tellduslive/binary_sensor.py b/homeassistant/components/tellduslive/binary_sensor.py index 09541a120fd..3f829365689 100644 --- a/homeassistant/components/tellduslive/binary_sensor.py +++ b/homeassistant/components/tellduslive/binary_sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.components import binary_sensor, tellduslive -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from .entry import TelldusLiveEntity @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice): +class TelldusLiveSensor(TelldusLiveEntity, BinarySensorEntity): """Representation of a Tellstick sensor.""" @property diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 6e31cd595bf..246b22dc157 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -2,7 +2,7 @@ import logging from homeassistant.components import cover, tellduslive -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from .entry import TelldusLiveEntity @@ -25,7 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TelldusLiveCover(TelldusLiveEntity, CoverDevice): +class TelldusLiveCover(TelldusLiveEntity, CoverEntity): """Representation of a cover.""" @property diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 3087c4cdf08..76d26a2fd94 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -2,7 +2,11 @@ import logging from homeassistant.components import light, tellduslive -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .entry import TelldusLiveEntity @@ -25,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TelldusLiveLight(TelldusLiveEntity, Light): +class TelldusLiveLight(TelldusLiveEntity, LightEntity): """Representation of a Tellstick Net light.""" def __init__(self, client, device_id): diff --git a/homeassistant/components/tellduslive/strings.json b/homeassistant/components/tellduslive/strings.json index d94b8965ce8..c29916be936 100644 --- a/homeassistant/components/tellduslive/strings.json +++ b/homeassistant/components/tellduslive/strings.json @@ -6,13 +6,20 @@ "authorize_url_timeout": "Timeout generating authorize url.", "unknown": "Unknown error occurred" }, - "error": { "auth_error": "Authentication error, please try again" }, + "error": { + "auth_error": "Authentication error, please try again" + }, "step": { "auth": { "description": "To link your TelldusLive account:\n 1. Click the link below\n 2. Login to Telldus Live\n 3. Authorize **{app_name}** (click **Yes**).\n 4. Come back here and click **SUBMIT**.\n\n [Link TelldusLive account]({auth_url})", "title": "Authenticate against TelldusLive" }, - "user": { "data": { "host": "Host" }, "title": "Pick endpoint." } + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "title": "Pick endpoint." + } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/ca.json b/homeassistant/components/tellduslive/translations/ca.json index 7a467844c75..c729dea0dd0 100644 --- a/homeassistant/components/tellduslive/translations/ca.json +++ b/homeassistant/components/tellduslive/translations/ca.json @@ -19,7 +19,7 @@ "host": "Amfitri\u00f3" }, "description": "buit", - "title": "Selecci\u00f3 extrem" + "title": "Selecci\u00f3 de l'endpoint" } } } diff --git a/homeassistant/components/tellduslive/translations/es-419.json b/homeassistant/components/tellduslive/translations/es-419.json index 36b358192be..71529c1f41d 100644 --- a/homeassistant/components/tellduslive/translations/es-419.json +++ b/homeassistant/components/tellduslive/translations/es-419.json @@ -3,6 +3,7 @@ "abort": { "already_setup": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" }, "error": { @@ -16,7 +17,8 @@ "user": { "data": { "host": "Host" - } + }, + "title": "Elegir punto final." } } } diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json index 7851f9d64bf..fa3cb937baf 100644 --- a/homeassistant/components/tellduslive/translations/ko.json +++ b/homeassistant/components/tellduslive/translations/ko.json @@ -12,14 +12,14 @@ "step": { "auth": { "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live \uc5d0 \ub85c\uadf8\uc778 \ud558\uc138\uc694\n 3. Authorize **{app_name}** (**Yes** \ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **SUBMIT** \uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})", - "title": "TelldusLive \uc778\uc99d" + "title": "TelldusLive \uc778\uc99d\ud558\uae30" }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "\uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc120\ud0dd" + "title": "\uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 9eb459e53e6..7ba0ed4c208 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_setup": "TelldusLive er allerede konfigurert", - "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", "unknown": "Ukjent feil oppstod" }, "error": { - "auth_error": "Autentiseringsfeil, vennligst pr\u00f8v igjen" + "auth_error": "Godkjenningsfeil, vennligst pr\u00f8v igjen" }, "step": { "auth": { diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index c07f9de7b3d..acf9df68b03 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -3,8 +3,8 @@ "abort": { "already_setup": "TelldusLive jest ju\u017c skonfigurowany.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "error": { "auth_error": "B\u0142\u0105d uwierzytelniania, spr\u00f3buj ponownie" @@ -16,7 +16,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP" + "host": "[%key_id:common::config_flow::data::host%]" }, "description": "Puste", "title": "Wybierz punkt ko\u0144cowy." diff --git a/homeassistant/components/tellstick/cover.py b/homeassistant/components/tellstick/cover.py index 0a5643fc1ea..bb25a601a2f 100644 --- a/homeassistant/components/tellstick/cover.py +++ b/homeassistant/components/tellstick/cover.py @@ -1,5 +1,5 @@ """Support for Tellstick covers.""" -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverEntity from . import ( ATTR_DISCOVER_CONFIG, @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class TellstickCover(TellstickDevice, CoverDevice): +class TellstickCover(TellstickDevice, CoverEntity): """Representation of a Tellstick cover.""" @property diff --git a/homeassistant/components/tellstick/light.py b/homeassistant/components/tellstick/light.py index cb4fe9b37ec..15b15112d14 100644 --- a/homeassistant/components/tellstick/light.py +++ b/homeassistant/components/tellstick/light.py @@ -1,5 +1,9 @@ """Support for Tellstick lights.""" -from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) from . import ( ATTR_DISCOVER_CONFIG, @@ -30,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class TellstickLight(TellstickDevice, Light): +class TellstickLight(TellstickDevice, LightEntity): """Representation of a Tellstick light.""" def __init__(self, tellcore_device, signal_repetitions): diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index a99fe044c46..e4d93d9f685 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - SwitchDevice, + SwitchEntity, ) from homeassistant.const import ( CONF_COMMAND_OFF, @@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class TelnetSwitch(SwitchDevice): +class TelnetSwitch(SwitchEntity): """Representation of a switch that can be toggled using telnet commands.""" def __init__( diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 937119ff6d4..3d6ac1dbe0e 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -7,7 +7,7 @@ from homeassistant.components.alarm_control_panel import ( ENTITY_ID_FORMAT, FORMAT_NUMBER, PLATFORM_SCHEMA, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -117,7 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(alarm_control_panels) -class AlarmControlPanelTemplate(AlarmControlPanel): +class AlarmControlPanelTemplate(AlarmControlPanelEntity): """Representation of a templated Alarm Control Panel.""" def __init__( diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index df918d3dd77..94d0f9d597b 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -107,7 +107,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class BinarySensorTemplate(BinarySensorDevice): +class BinarySensorTemplate(BinarySensorEntity): """A virtual binary sensor that triggers from another sensor.""" def __init__( diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 3e3232f2b91..e8bdebe2f58 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -17,7 +17,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, - CoverDevice, + CoverEntity, ) from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -161,7 +161,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(covers) -class CoverTemplate(CoverDevice): +class CoverTemplate(CoverEntity): """Representation of a Template cover.""" def __init__( @@ -297,7 +297,7 @@ class CoverTemplate(CoverDevice): if self._position_script is not None: supported_features |= SUPPORT_SET_POSITION - if self.current_cover_tilt_position is not None: + if self._tilt_script is not None: supported_features |= TILT_FEATURES return supported_features diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 1c19dfb33a6..df0a095cdd1 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -146,7 +146,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(lights) -class LightTemplate(Light): +class LightTemplate(LightEntity): """Representation of a templated Light, including dimmable.""" def __init__( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index a5caac00123..7a50e34f8cb 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice +from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, @@ -72,7 +72,7 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N ) -class TemplateLock(LockDevice): +class TemplateLock(LockEntity): """Representation of a template lock.""" def __init__( diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f96ed5479b9..0ec0eca553a 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.switch import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - SwitchDevice, + SwitchEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(switches) -class SwitchTemplate(SwitchDevice): +class SwitchTemplate(SwitchEntity): """Representation of a Template switch.""" def __init__( diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 4946d54edc3..c345663ca98 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -28,7 +28,7 @@ from homeassistant.components.vacuum import ( SUPPORT_START, SUPPORT_STATE, SUPPORT_STOP, - StateVacuumDevice, + StateVacuumEntity, ) from homeassistant.const import ( CONF_ENTITY_ID, @@ -144,7 +144,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(vacuums) -class TemplateVacuum(StateVacuumDevice): +class TemplateVacuum(StateVacuumEntity): """A template vacuum component.""" def __init__( diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 2ea292e8ff5..cbbd2d6345b 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,9 +4,9 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.18.2", + "numpy==1.18.4", "protobuf==3.6.1", - "pillow==7.1.1" + "pillow==7.1.2" ], "codeowners": [] } diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 8b60cd00163..c1f6fe18b99 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Tesla binary sensor.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): +class TeslaBinarySensor(TeslaDevice, BinarySensorEntity): """Implement an Tesla binary sensor for parking and charger.""" def __init__(self, tesla_device, controller, sensor_type, config_entry): diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 5ba6f182c0a..31269155d91 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -4,7 +4,7 @@ from typing import List, Optional from teslajsonpy.exceptions import UnknownPresetMode -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TeslaThermostat(TeslaDevice, ClimateDevice): +class TeslaThermostat(TeslaDevice, ClimateEntity): """Representation of a Tesla climate.""" def __init__(self, tesla_device, controller, config_entry): diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 7dffff5a5e0..91833d777fd 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -1,7 +1,7 @@ """Support for Tesla door locks.""" import logging -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -22,7 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class TeslaLock(TeslaDevice, LockDevice): +class TeslaLock(TeslaDevice, LockEntity): """Representation of a Tesla door lock.""" def __init__(self, tesla_device, controller, config_entry): diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json index 7a15e5d35d9..fb3c11a276f 100644 --- a/homeassistant/components/tesla/strings.json +++ b/homeassistant/components/tesla/strings.json @@ -8,7 +8,10 @@ }, "step": { "user": { - "data": { "username": "Email Address", "password": "Password" }, + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + }, "description": "Please enter your information.", "title": "Tesla - Configuration" } @@ -24,4 +27,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 716836821c4..5e9c2aa9031 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -1,7 +1,7 @@ """Support for Tesla charger switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -24,7 +24,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class ChargerSwitch(TeslaDevice, SwitchDevice): +class ChargerSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla charger switch.""" def __init__(self, tesla_device, controller, config_entry): @@ -54,7 +54,7 @@ class ChargerSwitch(TeslaDevice, SwitchDevice): self._state = STATE_ON if self.tesla_device.is_charging() else STATE_OFF -class RangeSwitch(TeslaDevice, SwitchDevice): +class RangeSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla max range charging switch.""" def __init__(self, tesla_device, controller, config_entry): @@ -84,7 +84,7 @@ class RangeSwitch(TeslaDevice, SwitchDevice): self._state = bool(self.tesla_device.is_maxrange()) -class UpdateSwitch(TeslaDevice, SwitchDevice): +class UpdateSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla update switch.""" def __init__(self, tesla_device, controller, config_entry): @@ -118,7 +118,7 @@ class UpdateSwitch(TeslaDevice, SwitchDevice): self._state = bool(self.controller.get_updates(car_id)) -class SentryModeSwitch(TeslaDevice, SwitchDevice): +class SentryModeSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla sentry mode switch.""" async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/tesla/translations/en.json b/homeassistant/components/tesla/translations/en.json index 34fdb66c098..762a7f01d0e 100644 --- a/homeassistant/components/tesla/translations/en.json +++ b/homeassistant/components/tesla/translations/en.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "Email Address" + "username": "Email" }, "description": "Please enter your information.", "title": "Tesla - Configuration" diff --git a/homeassistant/components/tesla/translations/es-419.json b/homeassistant/components/tesla/translations/es-419.json new file mode 100644 index 00000000000..d29077e688d --- /dev/null +++ b/homeassistant/components/tesla/translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error al conectar verifique la red y vuelva a intentar", + "identifier_exists": "correo electr\u00f3nico ya registrado", + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unknown_error": "Error desconocido, informe la informaci\u00f3n del registro" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor ingrese su informaci\u00f3n.", + "title": "Tesla - Configuraci\u00f3n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "Forzar a autom\u00f3viles despertar al inicio", + "scan_interval": "Segundos entre escaneos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/ko.json b/homeassistant/components/tesla/translations/ko.json index 24cb45a5c45..4e48539ccc0 100644 --- a/homeassistant/components/tesla/translations/ko.json +++ b/homeassistant/components/tesla/translations/ko.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + "username": "\uc774\uba54\uc77c" }, "description": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "Tesla - \uad6c\uc131" diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json index 02bbd32e417..27df1acae2a 100644 --- a/homeassistant/components/tesla/translations/nl.json +++ b/homeassistant/components/tesla/translations/nl.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Forceer auto's wakker bij het opstarten", "scan_interval": "Seconden tussen scans" } } diff --git a/homeassistant/components/tesla/translations/no.json b/homeassistant/components/tesla/translations/no.json index 1593319c8e1..a2ca81bf693 100644 --- a/homeassistant/components/tesla/translations/no.json +++ b/homeassistant/components/tesla/translations/no.json @@ -3,7 +3,7 @@ "error": { "connection_error": "Feil ved tilkobling; sjekk nettverket og pr\u00f8v p\u00e5 nytt", "identifier_exists": "E-post er allerede registrert", - "invalid_credentials": "Ugyldig brukerinformasjon", + "invalid_credentials": "Ugyldig legitimasjon", "unknown_error": "Ukjent feil, Vennligst rapporter informasjon fra Loggen" }, "step": { @@ -12,7 +12,7 @@ "password": "Passord", "username": "E-postadresse" }, - "description": "Vennligst skriv inn informasjonen din.", + "description": "Vennligst fyll inn din informasjonen.", "title": "Tesla - Konfigurasjon" } } diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json index e2a6b6a09c8..9054268f4a5 100644 --- a/homeassistant/components/tesla/translations/pl.json +++ b/homeassistant/components/tesla/translations/pl.json @@ -9,8 +9,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%]" }, "description": "Wprowad\u017a dane", "title": "Tesla - konfiguracja" diff --git a/homeassistant/components/tesla/translations/zh-Hant.json b/homeassistant/components/tesla/translations/zh-Hant.json index 522b42dd500..2668c29084c 100644 --- a/homeassistant/components/tesla/translations/zh-Hant.json +++ b/homeassistant/components/tesla/translations/zh-Hant.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u5bc6\u78bc", - "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + "username": "\u96fb\u5b50\u90f5\u4ef6" }, "description": "\u8acb\u8f38\u5165\u8cc7\u8a0a\u3002", "title": "Tesla - \u8a2d\u5b9a" diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 6d23018e897..1e9bb86d8fd 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -6,7 +6,7 @@ import logging from pytfiac import Tfiac import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, FAN_HIGH, @@ -73,7 +73,7 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N async_add_devices([TfiacClimate(hass, tfiac_client)]) -class TfiacClimate(ClimateDevice): +class TfiacClimate(ClimateEntity): """TFIAC class.""" def __init__(self, hass, client): diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 509d8cfd4d3..98d426cb660 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -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 ( ATTR_ENTITY_ID, @@ -75,7 +75,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class ThresholdSensor(BinarySensorDevice): +class ThresholdSensor(BinarySensorEntity): """Representation of a Threshold sensor.""" def __init__(self, hass, entity_id, name, lower, upper, hysteresis, device_class): diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 53c02a1461a..657be67c7fc 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -6,16 +6,19 @@ import aiohttp import tibber import voluptuous as vol +from homeassistant import config_entries from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_call_later from homeassistant.util import dt as dt_util -DOMAIN = "tibber" +from .const import DATA_HASS_CONFIG, DOMAIN -FIRST_RETRY_TIME = 60 +PLATFORMS = [ + "sensor", +] CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, @@ -25,12 +28,30 @@ CONFIG_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): +async def async_setup(hass, config): """Set up the Tibber component.""" - conf = config.get(DOMAIN) + + hass.data[DATA_HASS_CONFIG] = config + + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config[DOMAIN], + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" tibber_connection = tibber.Tibber( - conf[CONF_ACCESS_TOKEN], + access_token=entry.data[CONF_ACCESS_TOKEN], websession=async_get_clientsession(hass), time_zone=dt_util.DEFAULT_TIME_ZONE, ) @@ -44,15 +65,7 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): try: await tibber_connection.update_info() except asyncio.TimeoutError: - _LOGGER.warning("Timeout connecting to Tibber. Will retry in %ss", retry_delay) - - async def retry_setup(now): - """Retry setup if a timeout happens on Tibber API.""" - await async_setup(hass, config, retry_delay=min(2 * retry_delay, 900)) - - async_call_later(hass, retry_delay, retry_setup) - - return True + raise ConfigEntryNotReady except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber: %s ", err) return False @@ -60,7 +73,34 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): _LOGGER.error("Failed to login. %s", exp) return False - for component in ["sensor", "notify"]: - discovery.load_platform(hass, component, DOMAIN, {CONF_NAME: DOMAIN}, config) + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, "notify", DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DATA_HASS_CONFIG] + ) + ) 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, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + tibber_connection = hass.data.get(DOMAIN) + await tibber_connection.rt_disconnect() + + return unload_ok diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py new file mode 100644 index 00000000000..b0115d84e2c --- /dev/null +++ b/homeassistant/components/tibber/config_flow.py @@ -0,0 +1,68 @@ +"""Adds config flow for Tibber integration.""" +import asyncio +import logging + +import aiohttp +import tibber +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + + +class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tibber integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_import(self, import_info): + """Set the config entry up from yaml.""" + return await self.async_step_user(import_info) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + access_token = user_input[CONF_ACCESS_TOKEN].replace(" ", "") + + tibber_connection = tibber.Tibber( + access_token=access_token, + websession=async_get_clientsession(self.hass), + ) + + errors = {} + + try: + await tibber_connection.update_info() + except asyncio.TimeoutError: + errors[CONF_ACCESS_TOKEN] = "timeout" + except aiohttp.ClientError: + errors[CONF_ACCESS_TOKEN] = "connection_error" + except tibber.InvalidLogin: + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors, + ) + + unique_id = tibber_connection.user_id + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=tibber_connection.name, data={CONF_ACCESS_TOKEN: access_token}, + ) + + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors={},) diff --git a/homeassistant/components/tibber/const.py b/homeassistant/components/tibber/const.py new file mode 100644 index 00000000000..a35fa89c40f --- /dev/null +++ b/homeassistant/components/tibber/const.py @@ -0,0 +1,5 @@ +"""Constants for Tibber integration.""" + +DATA_HASS_CONFIG = "tibber_hass_config" +DOMAIN = "tibber" +MANUFACTURER = "Tibber" diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 78249a96291..36f4002949b 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,8 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.13.8"], + "requirements": ["pyTibber==0.14.0"], "codeowners": ["@danielhiversen"], - "quality_scale": "silver" + "quality_scale": "silver", + "config_flow": true } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 36f1a65222c..7fc8820e92d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -10,7 +10,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, dt as dt_util -from . import DOMAIN as TIBBER_DOMAIN +from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -20,10 +20,8 @@ SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tibber sensor.""" - if discovery_info is None: - return tibber_connection = hass.data.get(TIBBER_DOMAIN) @@ -66,11 +64,34 @@ class TibberSensor(Entity): """Return the state attributes.""" return self._device_state_attributes + @property + def model(self): + """Return the model of the sensor.""" + return None + @property def state(self): """Return the state of the device.""" return self._state + @property + def device_id(self): + """Return the ID of the physical device this sensor is part of.""" + home = self._tibber_home.info["viewer"]["home"] + return home["meteringPointData"]["consumptionEan"] + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(TIBBER_DOMAIN, self.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + } + if self.model is not None: + device_info["model"] = self.model + return device_info + class TibberSensorElPrice(TibberSensor): """Representation of a Tibber sensor for el price.""" @@ -112,6 +133,11 @@ class TibberSensorElPrice(TibberSensor): """Return the name of the sensor.""" return f"Electricity price {self._name}" + @property + def model(self): + """Return the model of the sensor.""" + return "Price Sensor" + @property def icon(self): """Return the icon to use in the frontend.""" @@ -125,8 +151,7 @@ class TibberSensorElPrice(TibberSensor): @property def unique_id(self): """Return a unique ID.""" - home = self._tibber_home.info["viewer"]["home"] - return home["meteringPointData"]["consumptionEan"] + return self.device_id @Throttle(MIN_TIME_BETWEEN_UPDATES) async def _fetch_data(self): @@ -149,7 +174,7 @@ class TibberSensorRT(TibberSensor): """Representation of a Tibber sensor for real time consumption.""" async def async_added_to_hass(self): - """Start unavailability tracking.""" + """Start listen for real time data.""" await self._tibber_home.rt_subscribe(self.hass.loop, self._async_callback) async def _async_callback(self, payload): @@ -177,6 +202,11 @@ class TibberSensorRT(TibberSensor): """Return True if entity is available.""" return self._tibber_home.rt_subscription_running + @property + def model(self): + """Return the model of the sensor.""" + return "Tibber Pulse" + @property def name(self): """Return the name of the sensor.""" @@ -200,6 +230,4 @@ class TibberSensorRT(TibberSensor): @property def unique_id(self): """Return a unique ID.""" - home = self._tibber_home.info["viewer"]["home"] - _id = home["meteringPointData"]["consumptionEan"] - return f"{_id}_rt_consumption" + return f"{self.device_id}_rt_consumption" diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json new file mode 100644 index 00000000000..cb5cc08552a --- /dev/null +++ b/homeassistant/components/tibber/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Tibber", + "config": { + "abort": { + "already_configured": "A Tibber account is already configured." + }, + "error": { + "timeout": "Timeout connecting to Tibber", + "connection_error": "Error connecting to Tibber", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" + }, + "step": { + "user": { + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/ca.json b/homeassistant/components/tibber/translations/ca.json new file mode 100644 index 00000000000..0b17cbb3524 --- /dev/null +++ b/homeassistant/components/tibber/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ja hi ha un compte Tibber configurat." + }, + "error": { + "connection_error": "S'ha produ\u00eft un error en connectar-se a Tibber", + "invalid_access_token": "[%key::common::config_flow::error::invalid_access_token%]", + "timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 a Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Token d'acc\u00e9s" + }, + "description": "Introdueix el token d'acc\u00e9s de https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json new file mode 100644 index 00000000000..8ac2219ba60 --- /dev/null +++ b/homeassistant/components/tibber/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ein Tibber-Konto ist bereits konfiguriert." + }, + "error": { + "connection_error": "Fehler beim Verbinden mit Tibber", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "timeout": "Zeit\u00fcberschreitung beim Verbinden mit Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Zugriffs-Token" + }, + "description": "Geben Sie Ihr Zugangsk\u00fcrzel von https://developer.tibber.com/settings/accesstoken ein.", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/en.json b/homeassistant/components/tibber/translations/en.json new file mode 100644 index 00000000000..658ce004943 --- /dev/null +++ b/homeassistant/components/tibber/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A Tibber account is already configured." + }, + "error": { + "connection_error": "Error connecting to Tibber", + "invalid_access_token": "Invalid access token", + "timeout": "Timeout connecting to Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Access Token" + }, + "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/es.json b/homeassistant/components/tibber/translations/es.json new file mode 100644 index 00000000000..31d8bc1a0ae --- /dev/null +++ b/homeassistant/components/tibber/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Una cuenta de Tibber ya est\u00e1 configurada." + }, + "error": { + "connection_error": "Error de conexi\u00f3n con Tibber", + "invalid_access_token": "Token de acceso inv\u00e1lido", + "timeout": "Tiempo de espera para conectarse a Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso" + }, + "description": "Introduzca su token de acceso desde https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/fi.json b/homeassistant/components/tibber/translations/fi.json new file mode 100644 index 00000000000..ff270732fd9 --- /dev/null +++ b/homeassistant/components/tibber/translations/fi.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/fr.json b/homeassistant/components/tibber/translations/fr.json new file mode 100644 index 00000000000..a54522a8585 --- /dev/null +++ b/homeassistant/components/tibber/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Un compte Tibber est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "connection_error": "Erreur de connexion \u00e0 Tibber", + "timeout": "D\u00e9lai de connexion \u00e0 Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Jeton d'acc\u00e8s" + }, + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/he.json b/homeassistant/components/tibber/translations/he.json new file mode 100644 index 00000000000..97e67c3c5e7 --- /dev/null +++ b/homeassistant/components/tibber/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05d7\u05e9\u05d1\u05d5\u05df Tibber \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05beTibber \u05e0\u05db\u05e9\u05dc\u05d4", + "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05beTibber" + }, + "step": { + "user": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05de\u05behttps://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/it.json b/homeassistant/components/tibber/translations/it.json new file mode 100644 index 00000000000..3a5548360cf --- /dev/null +++ b/homeassistant/components/tibber/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Un account Tibber \u00e8 gi\u00e0 configurato." + }, + "error": { + "connection_error": "Errore durante la connessione a Tibber", + "invalid_access_token": "Token di accesso non valido", + "timeout": "Tempo scaduto per la connessione a Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso" + }, + "description": "Immettere il token di accesso da https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/ko.json b/homeassistant/components/tibber/translations/ko.json new file mode 100644 index 00000000000..92b777e35bb --- /dev/null +++ b/homeassistant/components/tibber/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Tibber \uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Tibber \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "timeout": "Tibber \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + }, + "description": "https://developer.tibber.com/settings/accesstoken \uc5d0\uc11c \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/lb.json b/homeassistant/components/tibber/translations/lb.json new file mode 100644 index 00000000000..90230d20eea --- /dev/null +++ b/homeassistant/components/tibber/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ee Tibber Kont ass scho konfigur\u00e9iert." + }, + "error": { + "connection_error": "Feeler beim verbannen mat Tibber", + "invalid_access_token": "Ong\u00ebltegen Acc\u00e8s Jeton", + "timeout": "Z\u00e4it Iwwerschreidung beim verbannen mat Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Acc\u00e8ss Jeton" + }, + "description": "F\u00ebll d\u00e4in Acc\u00e8s Jeton vun https://developer.tibber.com/settings/accesstoken aus", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/no.json b/homeassistant/components/tibber/translations/no.json new file mode 100644 index 00000000000..34e078f5467 --- /dev/null +++ b/homeassistant/components/tibber/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "En Tibber-konto er allerede konfigurert." + }, + "error": { + "connection_error": "Feil ved tilkobling til Tibber", + "invalid_access_token": "Ugyldig tilgangstoken", + "timeout": "Tidsavbrudd for tilkobling til Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Tilgangstoken" + }, + "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/pl.json b/homeassistant/components/tibber/translations/pl.json new file mode 100644 index 00000000000..9e6417c33d7 --- /dev/null +++ b/homeassistant/components/tibber/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "connection_error": "B\u0142\u0105d po\u0142\u0105czenia z Tibber.", + "invalid_access_token": "[%key_id:common::config_flow::error::invalid_access_token%]", + "timeout": "Przekroczono limit czasu \u0142\u0105czenia z Tibber." + }, + "step": { + "user": { + "data": { + "access_token": "[%key_id:common::config_flow::data::access_token%]" + }, + "description": "Wprowad\u017a token dost\u0119pu z https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/ru.json b/homeassistant/components/tibber/translations/ru.json new file mode 100644 index 00000000000..06a5bf1331f --- /dev/null +++ b/homeassistant/components/tibber/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/sl.json b/homeassistant/components/tibber/translations/sl.json new file mode 100644 index 00000000000..e97a2aaf031 --- /dev/null +++ b/homeassistant/components/tibber/translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun Tibber je \u017ee konfiguriran." + }, + "error": { + "connection_error": "Napaka pri povezovanju s Tibberjem", + "invalid_access_token": "Neveljaven dostopni \u017eeton", + "timeout": "\u010casovna omejitev za priklop na Tibber je potekla" + }, + "step": { + "user": { + "data": { + "access_token": "Dostopni \u017eeton" + }, + "description": "Vnesite svoj dostopni \u017eeton s strani https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/zh-Hant.json b/homeassistant/components/tibber/translations/zh-Hant.json new file mode 100644 index 00000000000..0521ff79266 --- /dev/null +++ b/homeassistant/components/tibber/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Tibber \u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "connection_error": "\u9023\u7dda\u81f3 Tibber \u932f\u8aa4", + "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548", + "timeout": "\u9023\u7dda\u81f3 Tibber \u903e\u6642" + }, + "step": { + "user": { + "data": { + "access_token": "\u5b58\u53d6\u5bc6\u9470" + }, + "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u5bc6\u9470", + "title": "Tibber" + } + } + }, + "title": "Tibber" +} \ No newline at end of file diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 6c623f29f18..22e18d9697b 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(lights) -class TikteckLight(Light): +class TikteckLight(LightEntity): """Representation of a Tikteck light.""" def __init__(self, device): diff --git a/homeassistant/components/timer/translations/es.json b/homeassistant/components/timer/translations/es.json index b180c81a18e..8445d259a15 100644 --- a/homeassistant/components/timer/translations/es.json +++ b/homeassistant/components/timer/translations/es.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "activo", - "idle": "inactivo", - "paused": "pausado" + "active": "Activo", + "idle": "Inactivo", + "paused": "En pausa" } } } \ No newline at end of file diff --git a/homeassistant/components/timer/translations/it.json b/homeassistant/components/timer/translations/it.json index 464a2feb501..b5627e51d0b 100644 --- a/homeassistant/components/timer/translations/it.json +++ b/homeassistant/components/timer/translations/it.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "attivo", - "idle": "inattivo", - "paused": "in pausa" + "active": "Attivo", + "idle": "Inattivo", + "paused": "In pausa" } } } \ No newline at end of file diff --git a/homeassistant/components/timer/translations/ko.json b/homeassistant/components/timer/translations/ko.json index 5350e64524e..5ad1d79ba42 100644 --- a/homeassistant/components/timer/translations/ko.json +++ b/homeassistant/components/timer/translations/ko.json @@ -3,7 +3,7 @@ "_": { "active": "\ud65c\uc131\ud654", "idle": "\ub300\uae30\uc911", - "paused": "\uc77c\uc2dc\uc911\uc9c0\ub428" + "paused": "\uc77c\uc2dc\uc911\uc9c0" } } } \ No newline at end of file diff --git a/homeassistant/components/timer/translations/no.json b/homeassistant/components/timer/translations/no.json new file mode 100644 index 00000000000..431e4895c1a --- /dev/null +++ b/homeassistant/components/timer/translations/no.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "active": "Aktiv", + "idle": "Inaktiv", + "paused": "Pauset" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index ee9969c9974..8a5bbf16c6c 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -5,7 +5,7 @@ import logging import pytz import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ( CONF_AFTER, CONF_BEFORE, @@ -60,7 +60,7 @@ def is_sun_event(event): return event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) -class TodSensor(BinarySensorDevice): +class TodSensor(BinarySensorEntity): """Time of the Day Sensor.""" def __init__(self, name, after, after_offset, before, before_offset): diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index e6ef780ec8e..500cbec1526 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -3,7 +3,7 @@ import logging from typing import Any -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -110,7 +110,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class ToonBinarySensor(ToonEntity, BinarySensorDevice): +class ToonBinarySensor(ToonEntity, BinarySensorEntity): """Defines an Toon binary sensor.""" def __init__( diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index fac9cf4ffc2..f3c3d9a69bf 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -3,7 +3,7 @@ import logging from typing import Any, Dict, List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities([ToonThermostatDevice(toon_client, toon_data)], True) -class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice): +class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): """Representation of a Toon climate device.""" def __init__(self, toon_client, toon_data: ToonData) -> None: diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index 3ab64dafa24..897c398af9b 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -5,15 +5,17 @@ "title": "Link your Toon account", "description": "Authenticate with your Eneco Toon account (not the developer account).", "data": { - "username": "Username", - "password": "Password", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", "tenant": "Tenant" } }, "display": { "title": "Select display", "description": "Select the Toon display to connect with.", - "data": { "display": "Choose display" } + "data": { + "display": "Choose display" + } } }, "error": { @@ -28,4 +30,4 @@ "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/es-419.json b/homeassistant/components/toon/translations/es-419.json index 6fa63e591a3..af39c29971d 100644 --- a/homeassistant/components/toon/translations/es-419.json +++ b/homeassistant/components/toon/translations/es-419.json @@ -1,7 +1,10 @@ { "config": { "abort": { + "client_id": "La identificaci\u00f3n del cliente de la configuraci\u00f3n no es v\u00e1lida.", + "client_secret": "El secreto del cliente de la configuraci\u00f3n no es v\u00e1lido.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", + "no_app": "Debe configurar Toon antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." }, "error": { @@ -12,8 +15,10 @@ "authenticate": { "data": { "password": "Contrase\u00f1a", + "tenant": "Tenant", "username": "Nombre de usuario" }, + "description": "Autent\u00edquese con su cuenta de Eneco Toon (no con la cuenta de desarrollador).", "title": "Vincula tu cuenta de Toon" }, "display": { diff --git a/homeassistant/components/toon/translations/fi.json b/homeassistant/components/toon/translations/fi.json new file mode 100644 index 00000000000..e269670b21b --- /dev/null +++ b/homeassistant/components/toon/translations/fi.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "authenticate": { + "title": "Yhdist\u00e4 Toon-tili" + }, + "display": { + "data": { + "display": "Valitse n\u00e4ytt\u00f6" + }, + "title": "Valitse n\u00e4ytt\u00f6" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index 6940f8ca33f..200eb2d810b 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -19,14 +19,14 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "Eneco Toon \uacc4\uc815\uc73c\ub85c \uc778\uc99d\ud574\uc8fc\uc138\uc694. (\uac1c\ubc1c\uc790 \uacc4\uc815 \uc544\ub2d8)", - "title": "Toon \uacc4\uc815 \uc5f0\uacb0" + "title": "Toon \uacc4\uc815 \uc5f0\uacb0\ud558\uae30" }, "display": { "data": { "display": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" }, "description": "\uc5f0\uacb0\ud560 Toon \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" + "title": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index a41438b332a..89669e9b019 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -8,7 +8,7 @@ "unknown_auth_fail": "Det oppstod en uventet feil under godkjenning." }, "error": { - "credentials": "Den oppgitte kontoinformasjonen er ugyldig.", + "credentials": "De oppgitte legitimasjonene er ugyldige.", "display_exists": "Den valgte skjermen er allerede konfigurert." }, "step": { @@ -19,7 +19,7 @@ "username": "Brukernavn" }, "description": "Godkjenn med Eneco Toon kontoen din (ikke utviklerkontoen).", - "title": "Linken din Toon konto" + "title": "Koble til din Toon konto" }, "display": { "data": { diff --git a/homeassistant/components/toon/translations/pl.json b/homeassistant/components/toon/translations/pl.json index c3d783a9034..bbd9b03736d 100644 --- a/homeassistant/components/toon/translations/pl.json +++ b/homeassistant/components/toon/translations/pl.json @@ -5,7 +5,7 @@ "client_secret": "Tajny klucz klienta z konfiguracji jest nieprawid\u0142owy.", "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon.", "no_app": "Musisz skonfigurowa\u0107 Toon, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas uwierzytelniania." + "unknown_auth_fail": "Nieoczekiwany b\u0142\u0105d podczas uwierzytelniania." }, "error": { "credentials": "Wprowadzone dane logowania s\u0105 nieprawid\u0142owe.", @@ -14,9 +14,9 @@ "step": { "authenticate": { "data": { - "password": "Has\u0142o", + "password": "[%key_id:common::config_flow::data::password%]", "tenant": "Najemca", - "username": "Nazwa u\u017cytkownika" + "username": "[%key_id:common::config_flow::data::username%]" }, "description": "Uwierzytelnij konto Eneco Toon (nie konto programisty).", "title": "Po\u0142\u0105cz konto Toon" diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 2a32ae89b4a..632673233ec 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: async_add_entities(alarms, True) -class TotalConnectAlarm(alarm.AlarmControlPanel): +class TotalConnectAlarm(alarm.AlarmControlPanelEntity): """Represent an TotalConnect status.""" def __init__(self, name, location_id, client): diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 48d9a96a483..e296b12fa59 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_GAS, DEVICE_CLASS_SMOKE, - BinarySensorDevice, + BinarySensorEntity, ) from .const import DOMAIN @@ -26,7 +26,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: async_add_entities(sensors, True) -class TotalConnectBinarySensor(BinarySensorDevice): +class TotalConnectBinarySensor(BinarySensorEntity): """Represent an TotalConnect zone.""" def __init__(self, zone_id, location_id, zone): diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 0ce98c7c4d4..eb7a91ef438 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -3,10 +3,17 @@ "step": { "user": { "title": "Total Connect", - "data": { "username": "Username", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, - "error": { "login": "Login error: please check your username & password" }, - "abort": { "already_configured": "Account already configured" } + "error": { + "login": "Login error: please check your username & password" + }, + "abort": { + "already_configured": "Account already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 19106050c6d..ca9e6d66e2e 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El compte ja ha estat configurat" }, "error": { - "login": "Error d\u2019inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya" + "login": "Error d'inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya" }, "step": { "user": { diff --git a/homeassistant/components/totalconnect/translations/es-419.json b/homeassistant/components/totalconnect/translations/es-419.json new file mode 100644 index 00000000000..421d2667099 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su nombre de usuario y contrase\u00f1a" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index d2c211a7c7c..ef7cf7f38fa 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "login": "Erreur de connexion: veuillez v\u00e9rifier votre nom d'utilisateur et votre mot de passe" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Total Connect" + } } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json new file mode 100644 index 00000000000..508c112ae61 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Account al geconfigureerd" + }, + "error": { + "login": "Aanmeldingsfout: controleer uw gebruikersnaam en wachtwoord" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index f93f2cc4748..c312f98f3d2 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -12,7 +12,7 @@ "password": "Passord", "username": "Brukernavn" }, - "title": "Total Connect" + "title": "" } } } diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index 58a0f7018da..426dfebd60e 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Total Connect" } diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json new file mode 100644 index 00000000000..68dc9efeedb --- /dev/null +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "login": "Inloggningsfel: v\u00e4nligen kontrollera ditt anv\u00e4ndarnamn och l\u00f6senord" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 984e454ae02..218a6a420ff 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -5,7 +5,7 @@ from typing import List from pytouchline import PyTouchline import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class Touchline(ClimateDevice): +class Touchline(ClimateEntity): """Representation of a Touchline device.""" def __init__(self, touchline_thermostat): diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 9910fe42e03..9c3d73b43b5 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -13,7 +13,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.device_registry as dr @@ -120,7 +120,7 @@ class LightFeatures(NamedTuple): has_emeter: bool -class TPLinkSmartBulb(Light): +class TPLinkSmartBulb(LightEntity): """Representation of a TPLink Smart Bulb.""" def __init__(self, smartbulb: SmartBulb) -> None: diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 59d993477df..344f9cd96b0 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -7,7 +7,7 @@ from pyHS100 import SmartDeviceException, SmartPlug from homeassistant.components.switch import ( ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, - SwitchDevice, + SwitchEntity, ) from homeassistant.const import ATTR_VOLTAGE import homeassistant.helpers.device_registry as dr @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_ent return True -class SmartPlugSwitch(SwitchDevice): +class SmartPlugSwitch(SwitchEntity): """Representation of a TPLink Smart Plug switch.""" def __init__(self, smartplug: SmartPlug): diff --git a/homeassistant/components/tplink/translations/fi.json b/homeassistant/components/tplink/translations/fi.json new file mode 100644 index 00000000000..bc9be34a83e --- /dev/null +++ b/homeassistant/components/tplink/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/es-419.json b/homeassistant/components/traccar/translations/es-419.json index bfe62cc4e78..17f7560a464 100644 --- a/homeassistant/components/traccar/translations/es-419.json +++ b/homeassistant/components/traccar/translations/es-419.json @@ -3,6 +3,15 @@ "abort": { "not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes de Traccar.", "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n webhook en Traccar. \n\n Use la siguiente URL: `{webhook_url}` \n\n Consulte [la documentaci\u00f3n] ({docs_url}) para obtener m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfSeguro que desea configurar Traccar?", + "title": "Configurar Traccar" + } } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/ko.json b/homeassistant/components/traccar/translations/ko.json index 4d4282dc4c1..f44030821fd 100644 --- a/homeassistant/components/traccar/translations/ko.json +++ b/homeassistant/components/traccar/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "description": "Traccar \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Traccar \uc124\uc815" + "title": "Traccar \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/traccar/translations/no.json b/homeassistant/components/traccar/translations/no.json index 41a05ac1c6f..53e6500e70e 100644 --- a/homeassistant/components/traccar/translations/no.json +++ b/homeassistant/components/traccar/translations/no.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { - "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: ' {webhook_url} '\n\nSe [dokumentasjonen] ({docs_url}) for mer informasjon." + "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: `{webhook_url}`\n\nSe [dokumentasjonen]({docs_url}) for mer informasjon." }, "step": { "user": { diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index a797607e243..cef22c636c1 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -100,7 +100,6 @@ async def async_setup_entry(hass, entry): entry.data[CONF_HOST], psk_id=entry.data[CONF_IDENTITY], psk=entry.data[CONF_KEY], - loop=hass.loop, ) async def on_hass_stop(event): diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 048541b5402..2ade04cff55 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -1,6 +1,5 @@ """Config flow for Tradfri.""" import asyncio -from collections import OrderedDict from uuid import uuid4 import async_timeout @@ -70,7 +69,7 @@ class FlowHandler(config_entries.ConfigFlow): else: user_input = {} - fields = OrderedDict() + fields = {} if self._host is None: fields[vol.Required(CONF_HOST, default=user_input.get(CONF_HOST))] = str @@ -83,25 +82,28 @@ class FlowHandler(config_entries.ConfigFlow): step_id="auth", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_zeroconf(self, user_input): - """Handle zeroconf discovery.""" + async def async_step_homekit(self, user_input): + """Handle homekit discovery.""" + await self.async_set_unique_id(user_input["properties"]["id"]) + self._abort_if_unique_id_configured({CONF_HOST: user_input["host"]}) + host = user_input["host"] - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["host"] = host - - if any(host == flow["context"]["host"] for flow in self._async_in_progress()): - return self.async_abort(reason="already_in_progress") - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == host: - return self.async_abort(reason="already_configured") + if entry.data[CONF_HOST] != host: + continue + + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, unique_id=user_input["properties"]["id"] + ) + + return self.async_abort(reason="already_configured") self._host = host return await self.async_step_auth() - async_step_homekit = async_step_zeroconf - async def async_step_import(self, user_input): """Import a config entry.""" for entry in self._async_current_entries(): @@ -176,7 +178,7 @@ async def get_gateway_info(hass, host, identity, key): """Return info for the gateway.""" try: - factory = APIFactory(host, psk_id=identity, psk=key, loop=hass.loop) + factory = APIFactory(host, psk_id=identity, psk=key) api = factory.request gateway = Gateway() diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 744ba2e13b1..6d8669eea91 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,6 +1,6 @@ """Support for IKEA Tradfri covers.""" -from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.cover import ATTR_POSITION, CoverEntity from .base_class import TradfriBaseDevice from .const import ATTR_MODEL, CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY @@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) -class TradfriCover(TradfriBaseDevice, CoverDevice): +class TradfriCover(TradfriBaseDevice, CoverEntity): """The platform class required by Home Assistant.""" def __init__(self, device, api, gateway_id): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 40fe7b01cb0..4e44b452d33 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) import homeassistant.util.color as color_util @@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) -class TradfriGroup(TradfriBaseClass, Light): +class TradfriGroup(TradfriBaseClass, LightEntity): """The platform class for light groups required by hass.""" def __init__(self, device, api, gateway_id): @@ -106,7 +106,7 @@ class TradfriGroup(TradfriBaseClass, Light): await self._api(self._device.set_state(1)) -class TradfriLight(TradfriBaseDevice, Light): +class TradfriLight(TradfriBaseDevice, LightEntity): """The platform class required by Home Assistant.""" def __init__(self, device, api, gateway_id): diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index ce88766039b..12457975eee 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -7,6 +7,5 @@ "homekit": { "models": ["TRADFRI"] }, - "zeroconf": ["_coap._udp.local."], "codeowners": ["@ggravlingen"] } diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 5f33549260d..33a3f059098 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -4,7 +4,10 @@ "auth": { "title": "Enter security code", "description": "You can find the security code on the back of your gateway.", - "data": { "host": "Host", "security_code": "Security Code" } + "data": { + "host": "[%key:common::config_flow::data::host%]", + "security_code": "Security Code" + } } }, "error": { @@ -17,4 +20,4 @@ "already_in_progress": "Bridge configuration is already in progress." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index fffbf320c7e..cf23ffeb445 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,5 +1,5 @@ """Support for IKEA Tradfri switches.""" -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from .base_class import TradfriBaseDevice from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TradfriSwitch(TradfriBaseDevice, SwitchDevice): +class TradfriSwitch(TradfriBaseDevice, SwitchEntity): """The platform class required by Home Assistant.""" def __init__(self, device, api, gateway_id): diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 1c66cca87b6..cf1a1e81b5a 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "El puente ya esta configurado", - "already_in_progress": "La configuraci\u00f3n del bridge ya est\u00e1 en marcha." + "already_configured": "La pasarela ya est\u00e1 configurada", + "already_in_progress": "La configuraci\u00f3n de la pasarela ya est\u00e1 en marcha." }, "error": { "cannot_connect": "No se puede conectar a la puerta de enlace.", diff --git a/homeassistant/components/tradfri/translations/fi.json b/homeassistant/components/tradfri/translations/fi.json new file mode 100644 index 00000000000..5b7b417977d --- /dev/null +++ b/homeassistant/components/tradfri/translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "host": "Palvelin", + "security_code": "Turvakoodi" + }, + "title": "Kirjoita suojakoodi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/ko.json b/homeassistant/components/tradfri/translations/ko.json index a7fe2522c70..caa94fa8b10 100644 --- a/homeassistant/components/tradfri/translations/ko.json +++ b/homeassistant/components/tradfri/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\ube0c\ub9bf\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4." + "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\ube0c\ub9ac\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", @@ -16,7 +16,7 @@ "security_code": "\ubcf4\uc548 \ucf54\ub4dc" }, "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ub4b7\uba74\uc5d0\uc11c \ubcf4\uc548 \ucf54\ub4dc\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "\ubcf4\uc548 \ucf54\ub4dc \uc785\ub825" + "title": "\ubcf4\uc548 \ucf54\ub4dc \uc785\ub825\ud558\uae30" } } } diff --git a/homeassistant/components/tradfri/translations/no.json b/homeassistant/components/tradfri/translations/no.json index aed5dd1032b..39e66e48a78 100644 --- a/homeassistant/components/tradfri/translations/no.json +++ b/homeassistant/components/tradfri/translations/no.json @@ -16,7 +16,7 @@ "security_code": "Sikkerhetskode" }, "description": "Du finner sikkerhetskoden p\u00e5 baksiden av gatewayen din.", - "title": "Skriv inn sikkerhetskode" + "title": "Angi sikkerhetskode" } } } diff --git a/homeassistant/components/tradfri/translations/pl.json b/homeassistant/components/tradfri/translations/pl.json index 028956ec6b3..83190ee37e3 100644 --- a/homeassistant/components/tradfri/translations/pl.json +++ b/homeassistant/components/tradfri/translations/pl.json @@ -12,7 +12,7 @@ "step": { "auth": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "security_code": "Kod bezpiecze\u0144stwa" }, "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramki.", diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index ef8f49ab3d1..e16ef43a48e 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -5,10 +5,10 @@ "title": "Setup Transmission Client", "data": { "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port" + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]" } } }, @@ -17,14 +17,18 @@ "wrong_credentials": "Wrong username or password", "cannot_connect": "Unable to Connect to host" }, - "abort": { "already_configured": "Host is already configured." } + "abort": { + "already_configured": "Host is already configured." + } }, "options": { "step": { "init": { "title": "Configure options for Transmission", - "data": { "scan_interval": "Update frequency" } + "data": { + "scan_interval": "Update frequency" + } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 837766ba6ed..e2b014ac019 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" }, "title": "Opcions de configuraci\u00f3 de Transmission" } diff --git a/homeassistant/components/transmission/translations/es-419.json b/homeassistant/components/transmission/translations/es-419.json new file mode 100644 index 00000000000..002d03ee7d2 --- /dev/null +++ b/homeassistant/components/transmission/translations/es-419.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "name_exists": "El nombre ya existe", + "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Configurar cliente de transmisi\u00f3n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "title": "Configurar opciones para la transmisi\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/fi.json b/homeassistant/components/transmission/translations/fi.json new file mode 100644 index 00000000000..2edec783aff --- /dev/null +++ b/homeassistant/components/transmission/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Yhteyden muodostaminen palvelimeen ei onnistu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ko.json b/homeassistant/components/transmission/translations/ko.json index b82e80a342d..a82f56670b1 100644 --- a/homeassistant/components/transmission/translations/ko.json +++ b/homeassistant/components/transmission/translations/ko.json @@ -17,7 +17,7 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "Transmission \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815" + "title": "Transmission \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815\ud558\uae30" } } }, @@ -27,7 +27,7 @@ "data": { "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "title": "Transmission \uc635\uc158 \uc124\uc815" + "title": "Transmission \uc635\uc158 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index 33bd4d3bff4..f88e7c55ea4 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Kan ikke koble til vert", "name_exists": "Navnet eksisterer allerede", - "wrong_credentials": "Ugyldig brukernavn eller passord" + "wrong_credentials": "Feil brukernavn eller passord" }, "step": { "user": { @@ -14,7 +14,7 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "", + "port": "Port", "username": "Brukernavn" }, "title": "Oppsett av Transmission-klient" diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 52efb32b551..9d2b40f1cf2 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", + "host": "[%key_id:common::config_flow::data::host%]", "name": "Nazwa", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika" + "password": "[%key_id:common::config_flow::data::password%]", + "port": "[%key_id:common::config_flow::data::port%]", + "username": "[%key_id:common::config_flow::data::username%]" }, "title": "Konfiguracja klienta Transmission" } diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 7c4a2dc4067..1490efd7a86 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -95,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class SensorTrend(BinarySensorDevice): +class SensorTrend(BinarySensorEntity): """Representation of a trend Sensor.""" def __init__( diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index edd0bea977d..0d741bcf264 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.18.2"], + "requirements": ["numpy==1.18.4"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 1946207337b..9a33c0514d5 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -33,6 +33,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform @@ -114,7 +115,7 @@ async def async_setup(hass, config): use_cache = conf.get(CONF_CACHE, DEFAULT_CACHE) cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR) time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY) - base_url = conf.get(CONF_BASE_URL) or hass.config.api.base_url + base_url = conf.get(CONF_BASE_URL) or get_url(hass) await tts.async_init_cache(use_cache, cache_dir, time_memory, base_url) except (HomeAssistantError, KeyError) as err: @@ -437,9 +438,8 @@ class SpeechManager: album = provider.name artist = language - if options is not None: - if options.get("voice") is not None: - artist = options.get("voice") + if options is not None and options.get("voice") is not None: + artist = options.get("voice") try: tts_file = mutagen.File(data_bytes, easy=True) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 74b1d1439ad..4a522b76b8e 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,33 +1,44 @@ """Support for Tuya Smart devices.""" +import asyncio from datetime import timedelta import logging from tuyaha import TuyaApi +from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.core import callback -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_COUNTRYCODE, + DOMAIN, + TUYA_DATA, + TUYA_DISCOVERY_NEW, + TUYA_PLATFORMS, +) _LOGGER = logging.getLogger(__name__) -CONF_COUNTRYCODE = "country_code" +ENTRY_IS_SETUP = "tuya_entry_is_setup" PARALLEL_UPDATES = 0 -DOMAIN = "tuya" -DATA_TUYA = "data_tuya" +SERVICE_FORCE_UPDATE = "force_update" +SERVICE_PULL_DEVICES = "pull_devices" SIGNAL_DELETE_ENTITY = "tuya_delete" SIGNAL_UPDATE_ENTITY = "tuya_update" -SERVICE_FORCE_UPDATE = "force_update" -SERVICE_PULL_DEVICES = "pull_devices" - TUYA_TYPE_TO_HA = { "climate": "climate", "cover": "cover", @@ -37,35 +48,71 @@ TUYA_TYPE_TO_HA = { "switch": "switch", } +TUYA_TRACKER = "tuya_tracker" + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_COUNTRYCODE): cv.string, - vol.Optional(CONF_PLATFORM, default="tuya"): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_COUNTRYCODE): cv.string, + vol.Optional(CONF_PLATFORM, default="tuya"): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -def setup(hass, config): - """Set up Tuya Component.""" +async def async_setup(hass, config): + """Set up the Tuya integration.""" + + conf = config.get(DOMAIN) + if conf is not None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up Tuya platform.""" tuya = TuyaApi() - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - country_code = config[DOMAIN][CONF_COUNTRYCODE] - platform = config[DOMAIN][CONF_PLATFORM] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + country_code = entry.data[CONF_COUNTRYCODE] + platform = entry.data[CONF_PLATFORM] - hass.data[DATA_TUYA] = tuya - tuya.init(username, password, country_code, platform) - hass.data[DOMAIN] = {"entities": {}} + try: + await hass.async_add_executor_job( + tuya.init, username, password, country_code, platform + ) + except (TuyaNetException, TuyaServerException): + raise ConfigEntryNotReady() - def load_devices(device_list): + except TuyaAPIException as exc: + _LOGGER.error( + "Connection error during integration setup. Error: %s", exc, + ) + return False + + hass.data[DOMAIN] = { + TUYA_DATA: tuya, + TUYA_TRACKER: None, + ENTRY_IS_SETUP: set(), + "entities": {}, + "pending": {}, + } + + async def async_load_devices(device_list): """Load new devices by device_list.""" device_type_list = {} for device in device_list: @@ -79,51 +126,92 @@ def setup(hass, config): device_type_list[ha_type] = [] device_type_list[ha_type].append(device.object_id()) hass.data[DOMAIN]["entities"][device.object_id()] = None + for ha_type, dev_ids in device_type_list.items(): - discovery.load_platform(hass, ha_type, DOMAIN, {"dev_ids": dev_ids}, config) + config_entries_key = f"{ha_type}.tuya" + if config_entries_key not in hass.data[DOMAIN][ENTRY_IS_SETUP]: + hass.data[DOMAIN]["pending"][ha_type] = dev_ids + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, ha_type) + ) + hass.data[DOMAIN][ENTRY_IS_SETUP].add(config_entries_key) + else: + async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids) - device_list = tuya.get_all_devices() - load_devices(device_list) + device_list = await hass.async_add_executor_job(tuya.get_all_devices) + await async_load_devices(device_list) - def poll_devices_update(event_time): + def _get_updated_devices(): + tuya.poll_devices_update() + return tuya.get_all_devices() + + async def async_poll_devices_update(event_time): """Check if accesstoken is expired and pull device list from server.""" _LOGGER.debug("Pull devices from Tuya.") - tuya.poll_devices_update() # Add new discover device. - device_list = tuya.get_all_devices() - load_devices(device_list) + device_list = await hass.async_add_executor_job(_get_updated_devices) + await async_load_devices(device_list) # Delete not exist device. newlist_ids = [] for device in device_list: newlist_ids.append(device.object_id()) for dev_id in list(hass.data[DOMAIN]["entities"]): if dev_id not in newlist_ids: - dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id) + async_dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id) hass.data[DOMAIN]["entities"].pop(dev_id) - track_time_interval(hass, poll_devices_update, timedelta(minutes=5)) + hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval( + hass, async_poll_devices_update, timedelta(minutes=5) + ) - hass.services.register(DOMAIN, SERVICE_PULL_DEVICES, poll_devices_update) + hass.services.async_register( + DOMAIN, SERVICE_PULL_DEVICES, async_poll_devices_update + ) - def force_update(call): + async def async_force_update(call): """Force all devices to pull data.""" - dispatcher_send(hass, SIGNAL_UPDATE_ENTITY) + async_dispatcher_send(hass, SIGNAL_UPDATE_ENTITY) - hass.services.register(DOMAIN, SERVICE_FORCE_UPDATE, force_update) + hass.services.async_register(DOMAIN, SERVICE_FORCE_UPDATE, async_force_update) return True +async def async_unload_entry(hass, entry): + """Unloading the Tuya platforms.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload( + entry, component.split(".", 1)[0] + ) + for component in hass.data[DOMAIN][ENTRY_IS_SETUP] + ] + ) + ) + if unload_ok: + hass.data[DOMAIN][ENTRY_IS_SETUP] = set() + hass.data[DOMAIN][TUYA_TRACKER]() + hass.data[DOMAIN][TUYA_TRACKER] = None + hass.data[DOMAIN][TUYA_DATA] = None + hass.services.async_remove(DOMAIN, SERVICE_FORCE_UPDATE) + hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES) + hass.data.pop(DOMAIN) + + return unload_ok + + class TuyaDevice(Entity): """Tuya base device.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init Tuya devices.""" - self.tuya = tuya + self._tuya = tuya + self._tuya_platform = platform async def async_added_to_hass(self): """Call when entity is added to hass.""" - dev_id = self.tuya.object_id() + dev_id = self._tuya.object_id() self.hass.data[DOMAIN]["entities"][dev_id] = self.entity_id async_dispatcher_connect(self.hass, SIGNAL_DELETE_ENTITY, self._delete_callback) async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback) @@ -131,32 +219,50 @@ class TuyaDevice(Entity): @property def object_id(self): """Return Tuya device id.""" - return self.tuya.object_id() + return self._tuya.object_id() @property def unique_id(self): """Return a unique ID.""" - return f"tuya.{self.tuya.object_id()}" + return f"tuya.{self._tuya.object_id()}" @property def name(self): """Return Tuya device name.""" - return self.tuya.name() + return self._tuya.name() @property def available(self): """Return if the device is available.""" - return self.tuya.available() + return self._tuya.available() + + @property + def device_info(self): + """Return a device description for device registry.""" + _device_info = { + "identifiers": {(DOMAIN, f"{self.unique_id}")}, + "manufacturer": TUYA_PLATFORMS.get( + self._tuya_platform, self._tuya_platform + ), + "name": self.name, + "model": self._tuya.object_type(), + } + return _device_info def update(self): """Refresh Tuya device data.""" - self.tuya.update() + self._tuya.update() - @callback - def _delete_callback(self, dev_id): + async def _delete_callback(self, dev_id): """Remove this entity.""" if dev_id == self.object_id: - self.hass.async_create_task(self.async_remove()) + entity_registry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + if entity_registry.async_is_registered(self.entity_id): + entity_registry.async_remove(self.entity_id) + else: + await self.async_remove() @callback def _update_callback(self): diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index fe1fcc802ff..15cecefdb21 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,5 +1,9 @@ """Support for the Tuya climate devices.""" -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice +from homeassistant.components.climate import ( + DOMAIN as SENSOR_DOMAIN, + ENTITY_ID_FORMAT, + ClimateEntity, +) from homeassistant.components.climate.const import ( FAN_HIGH, FAN_LOW, @@ -14,12 +18,15 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_PLATFORM, PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TUYA, TuyaDevice +from . import TuyaDevice +from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW DEVICE_TYPE = "climate" @@ -37,34 +44,53 @@ TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH} -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya Climate devices.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get("dev_ids") - devices = [] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tuya sensors dynamically through tuya discovery.""" + + platform = config_entry.data[CONF_PLATFORM] + + async def async_discover_sensor(dev_ids): + """Discover and add a discovered tuya sensor.""" + if not dev_ids: + return + entities = await hass.async_add_executor_job( + _setup_entities, hass, dev_ids, platform, + ) + async_add_entities(entities) + + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + ) + + devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) + await async_discover_sensor(devices_ids) + + +def _setup_entities(hass, dev_ids, platform): + """Set up Tuya Climate device.""" + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: device = tuya.get_device_by_id(dev_id) if device is None: continue - devices.append(TuyaClimateDevice(device)) - add_entities(devices) + entities.append(TuyaClimateEntity(device, platform)) + return entities -class TuyaClimateDevice(TuyaDevice, ClimateDevice): +class TuyaClimateEntity(TuyaDevice, ClimateEntity): """Tuya climate devices,include air conditioner,heater.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init climate device.""" - super().__init__(tuya) + super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) self.operations = [HVAC_MODE_OFF] async def async_added_to_hass(self): """Create operation list when add to hass.""" await super().async_added_to_hass() - modes = self.tuya.operation_list() + modes = self._tuya.operation_list() if modes is None: return @@ -80,7 +106,7 @@ class TuyaClimateDevice(TuyaDevice, ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" - unit = self.tuya.temperature_unit() + unit = self._tuya.temperature_unit() if unit == "FAHRENHEIT": return TEMP_FAHRENHEIT return TEMP_CELSIUS @@ -88,10 +114,10 @@ class TuyaClimateDevice(TuyaDevice, ClimateDevice): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - if not self.tuya.state(): + if not self._tuya.state(): return HVAC_MODE_OFF - mode = self.tuya.current_operation() + mode = self._tuya.current_operation() if mode is None: return None return TUYA_STATE_TO_HA.get(mode) @@ -104,63 +130,63 @@ class TuyaClimateDevice(TuyaDevice, ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self.tuya.current_temperature() + return self._tuya.current_temperature() @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.tuya.target_temperature() + return self._tuya.target_temperature() @property def target_temperature_step(self): """Return the supported step of target temperature.""" - return self.tuya.target_temperature_step() + return self._tuya.target_temperature_step() @property def fan_mode(self): """Return the fan setting.""" - return self.tuya.current_fan_mode() + return self._tuya.current_fan_mode() @property def fan_modes(self): """Return the list of available fan modes.""" - return self.tuya.fan_list() + return self._tuya.fan_list() def set_temperature(self, **kwargs): """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: - self.tuya.set_temperature(kwargs[ATTR_TEMPERATURE]) + self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE]) def set_fan_mode(self, fan_mode): """Set new target fan mode.""" - self.tuya.set_fan_mode(fan_mode) + self._tuya.set_fan_mode(fan_mode) def set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: - self.tuya.turn_off() + self._tuya.turn_off() - if not self.tuya.state(): - self.tuya.turn_on() + if not self._tuya.state(): + self._tuya.turn_on() - self.tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode)) + self._tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode)) @property def supported_features(self): """Return the list of supported features.""" supports = 0 - if self.tuya.support_target_temperature(): + if self._tuya.support_target_temperature(): supports = supports | SUPPORT_TARGET_TEMPERATURE - if self.tuya.support_wind_speed(): + if self._tuya.support_wind_speed(): supports = supports | SUPPORT_FAN_MODE return supports @property def min_temp(self): """Return the minimum temperature.""" - return self.tuya.min_temp() + return self._tuya.min_temp() @property def max_temp(self): """Return the maximum temperature.""" - return self.tuya.max_temp() + return self._tuya.max_temp() diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py new file mode 100644 index 00000000000..c905334aac7 --- /dev/null +++ b/homeassistant/components/tuya/config_flow.py @@ -0,0 +1,108 @@ +"""Config flow for Tuya.""" +import logging + +from tuyaha import TuyaApi +from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME + +# pylint:disable=unused-import +from .const import CONF_COUNTRYCODE, DOMAIN, TUYA_PLATFORMS + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA_USER = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_COUNTRYCODE): vol.Coerce(int), + vol.Required(CONF_PLATFORM): vol.In(TUYA_PLATFORMS), + } +) + +RESULT_AUTH_FAILED = "auth_failed" +RESULT_CONN_ERROR = "conn_error" +RESULT_SUCCESS = "success" + +RESULT_LOG_MESSAGE = { + RESULT_AUTH_FAILED: "Invalid credential", + RESULT_CONN_ERROR: "Connection error", +} + + +class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a tuya config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self._country_code = None + self._password = None + self._platform = None + self._username = None + self._is_import = False + + def _get_entry(self): + return self.async_create_entry( + title=self._username, + data={ + CONF_COUNTRYCODE: self._country_code, + CONF_PASSWORD: self._password, + CONF_PLATFORM: self._platform, + CONF_USERNAME: self._username, + }, + ) + + def _try_connect(self): + """Try to connect and check auth.""" + tuya = TuyaApi() + try: + tuya.init( + self._username, self._password, self._country_code, self._platform + ) + except (TuyaNetException, TuyaServerException): + return RESULT_CONN_ERROR + except TuyaAPIException: + return RESULT_AUTH_FAILED + + return RESULT_SUCCESS + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + self._is_import = True + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + errors = {} + + if user_input is not None: + + self._country_code = str(user_input[CONF_COUNTRYCODE]) + self._password = user_input[CONF_PASSWORD] + self._platform = user_input[CONF_PLATFORM] + self._username = user_input[CONF_USERNAME] + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result == RESULT_SUCCESS: + return self._get_entry() + if result != RESULT_AUTH_FAILED or self._is_import: + if self._is_import: + _LOGGER.error( + "Error importing from configuration.yaml: %s", + RESULT_LOG_MESSAGE.get(result, "Generic Error"), + ) + return self.async_abort(reason=result) + errors["base"] = result + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors + ) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py new file mode 100644 index 00000000000..4e395750b23 --- /dev/null +++ b/homeassistant/components/tuya/const.py @@ -0,0 +1,14 @@ +"""Constants for the Tuya integration.""" + +CONF_COUNTRYCODE = "country_code" + +DOMAIN = "tuya" + +TUYA_DATA = "tuya_data" +TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}" + +TUYA_PLATFORMS = { + "tuya": "Tuya", + "smart_life": "Smart Life", + "jinvoo_smart": "Jinvoo Smart", +} diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 35fd4719fdb..538b819cb05 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -1,52 +1,75 @@ """Support for Tuya covers.""" from homeassistant.components.cover import ( + DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) +from homeassistant.const import CONF_PLATFORM +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TUYA, TuyaDevice +from . import TuyaDevice +from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW PARALLEL_UPDATES = 0 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya cover devices.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get("dev_ids") - devices = [] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tuya sensors dynamically through tuya discovery.""" + + platform = config_entry.data[CONF_PLATFORM] + + async def async_discover_sensor(dev_ids): + """Discover and add a discovered tuya sensor.""" + if not dev_ids: + return + entities = await hass.async_add_executor_job( + _setup_entities, hass, dev_ids, platform, + ) + async_add_entities(entities) + + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + ) + + devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) + await async_discover_sensor(devices_ids) + + +def _setup_entities(hass, dev_ids, platform): + """Set up Tuya Cover device.""" + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: device = tuya.get_device_by_id(dev_id) if device is None: continue - devices.append(TuyaCover(device)) - add_entities(devices) + entities.append(TuyaCover(device, platform)) + return entities -class TuyaCover(TuyaDevice, CoverDevice): +class TuyaCover(TuyaDevice, CoverEntity): """Tuya cover devices.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init tuya cover device.""" - super().__init__(tuya) + super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) @property def supported_features(self): """Flag supported features.""" supported_features = SUPPORT_OPEN | SUPPORT_CLOSE - if self.tuya.support_stop(): + if self._tuya.support_stop(): supported_features |= SUPPORT_STOP return supported_features @property def is_closed(self): """Return if the cover is closed or not.""" - state = self.tuya.state() + state = self._tuya.state() if state == 1: return False if state == 2: @@ -55,12 +78,12 @@ class TuyaCover(TuyaDevice, CoverDevice): def open_cover(self, **kwargs): """Open the cover.""" - self.tuya.open_cover() + self._tuya.open_cover() def close_cover(self, **kwargs): """Close cover.""" - self.tuya.close_cover() + self._tuya.close_cover() def stop_cover(self, **kwargs): """Stop the cover.""" - self.tuya.stop_cover() + self._tuya.stop_cover() diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 90cf452db5b..6144bd4ab96 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -1,67 +1,89 @@ """Support for Tuya fans.""" from homeassistant.components.fan import ( + DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import STATE_OFF +from homeassistant.const import CONF_PLATFORM, STATE_OFF +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TUYA, TuyaDevice +from . import TuyaDevice +from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW PARALLEL_UPDATES = 0 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya fan platform.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get("dev_ids") - devices = [] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tuya sensors dynamically through tuya discovery.""" + + platform = config_entry.data[CONF_PLATFORM] + + async def async_discover_sensor(dev_ids): + """Discover and add a discovered tuya sensor.""" + if not dev_ids: + return + entities = await hass.async_add_executor_job( + _setup_entities, hass, dev_ids, platform, + ) + async_add_entities(entities) + + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + ) + + devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) + await async_discover_sensor(devices_ids) + + +def _setup_entities(hass, dev_ids, platform): + """Set up Tuya Fan device.""" + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: device = tuya.get_device_by_id(dev_id) if device is None: continue - devices.append(TuyaFanDevice(device)) - add_entities(devices) + entities.append(TuyaFanDevice(device, platform)) + return entities class TuyaFanDevice(TuyaDevice, FanEntity): """Tuya fan devices.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init Tuya fan device.""" - super().__init__(tuya) + super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) self.speeds = [STATE_OFF] async def async_added_to_hass(self): """Create fan list when add to hass.""" await super().async_added_to_hass() - self.speeds.extend(self.tuya.speed_list()) + self.speeds.extend(self._tuya.speed_list()) def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" if speed == STATE_OFF: self.turn_off() else: - self.tuya.set_speed(speed) + self._tuya.set_speed(speed) def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" if speed is not None: self.set_speed(speed) else: - self.tuya.turn_on() + self._tuya.turn_on() def turn_off(self, **kwargs) -> None: """Turn the entity off.""" - self.tuya.turn_off() + self._tuya.turn_off() def oscillate(self, oscillating) -> None: """Oscillate the fan.""" - self.tuya.oscillate(oscillating) + self._tuya.oscillate(oscillating) @property def oscillating(self): @@ -70,18 +92,18 @@ class TuyaFanDevice(TuyaDevice, FanEntity): return None if self.speed == STATE_OFF: return False - return self.tuya.oscillating() + return self._tuya.oscillating() @property def is_on(self): """Return true if the entity is on.""" - return self.tuya.state() + return self._tuya.state() @property def speed(self) -> str: """Return the current speed.""" if self.is_on: - return self.tuya.speed() + return self._tuya.speed() return STATE_OFF @property @@ -93,6 +115,6 @@ class TuyaFanDevice(TuyaDevice, FanEntity): def supported_features(self) -> int: """Flag supported features.""" supports = SUPPORT_SET_SPEED - if self.tuya.support_oscillate(): + if self._tuya.support_oscillate(): supports = supports | SUPPORT_OSCILLATE return supports diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 6c05f4f2cc7..9416089f898 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -3,58 +3,81 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) +from homeassistant.const import CONF_PLATFORM +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import color as colorutil -from . import DATA_TUYA, TuyaDevice +from . import TuyaDevice +from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW PARALLEL_UPDATES = 0 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya light platform.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get("dev_ids") - devices = [] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tuya sensors dynamically through tuya discovery.""" + + platform = config_entry.data[CONF_PLATFORM] + + async def async_discover_sensor(dev_ids): + """Discover and add a discovered tuya sensor.""" + if not dev_ids: + return + entities = await hass.async_add_executor_job( + _setup_entities, hass, dev_ids, platform, + ) + async_add_entities(entities) + + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + ) + + devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) + await async_discover_sensor(devices_ids) + + +def _setup_entities(hass, dev_ids, platform): + """Set up Tuya Light device.""" + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: device = tuya.get_device_by_id(dev_id) if device is None: continue - devices.append(TuyaLight(device)) - add_entities(devices) + entities.append(TuyaLight(device, platform)) + return entities -class TuyaLight(TuyaDevice, Light): +class TuyaLight(TuyaDevice, LightEntity): """Tuya light device.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init Tuya light device.""" - super().__init__(tuya) + super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) @property def brightness(self): """Return the brightness of the light.""" - if self.tuya.brightness() is None: + if self._tuya.brightness() is None: return None - return int(self.tuya.brightness()) + return int(self._tuya.brightness()) @property def hs_color(self): """Return the hs_color of the light.""" - return tuple(map(int, self.tuya.hs_color())) + return tuple(map(int, self._tuya.hs_color())) @property def color_temp(self): """Return the color_temp of the light.""" - color_temp = int(self.tuya.color_temp()) + color_temp = int(self._tuya.color_temp()) if color_temp is None: return None return colorutil.color_temperature_kelvin_to_mired(color_temp) @@ -62,17 +85,17 @@ class TuyaLight(TuyaDevice, Light): @property def is_on(self): """Return true if light is on.""" - return self.tuya.state() + return self._tuya.state() @property def min_mireds(self): """Return color temperature min mireds.""" - return colorutil.color_temperature_kelvin_to_mired(self.tuya.min_color_temp()) + return colorutil.color_temperature_kelvin_to_mired(self._tuya.min_color_temp()) @property def max_mireds(self): """Return color temperature max mireds.""" - return colorutil.color_temperature_kelvin_to_mired(self.tuya.max_color_temp()) + return colorutil.color_temperature_kelvin_to_mired(self._tuya.max_color_temp()) def turn_on(self, **kwargs): """Turn on or control the light.""" @@ -81,27 +104,27 @@ class TuyaLight(TuyaDevice, Light): and ATTR_HS_COLOR not in kwargs and ATTR_COLOR_TEMP not in kwargs ): - self.tuya.turn_on() + self._tuya.turn_on() if ATTR_BRIGHTNESS in kwargs: - self.tuya.set_brightness(kwargs[ATTR_BRIGHTNESS]) + self._tuya.set_brightness(kwargs[ATTR_BRIGHTNESS]) if ATTR_HS_COLOR in kwargs: - self.tuya.set_color(kwargs[ATTR_HS_COLOR]) + self._tuya.set_color(kwargs[ATTR_HS_COLOR]) if ATTR_COLOR_TEMP in kwargs: color_temp = colorutil.color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP] ) - self.tuya.set_color_temp(color_temp) + self._tuya.set_color_temp(color_temp) def turn_off(self, **kwargs): """Instruct the light to turn off.""" - self.tuya.turn_off() + self._tuya.turn_off() @property def supported_features(self): """Flag supported features.""" supports = SUPPORT_BRIGHTNESS - if self.tuya.support_color(): + if self._tuya.support_color(): supports = supports | SUPPORT_COLOR - if self.tuya.support_color_temp(): + if self._tuya.support_color_temp(): supports = supports | SUPPORT_COLOR_TEMP return supports diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index cd6cb333020..8053dc8f697 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,6 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuyaha==0.0.5"], - "codeowners": [] + "requirements": ["tuyaha==0.0.6"], + "codeowners": ["@ollo69"], + "config_flow": true } diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 71d83417ca8..39613318379 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -1,38 +1,60 @@ """Support for the Tuya scenes.""" from typing import Any -from homeassistant.components.scene import DOMAIN, Scene +from homeassistant.components.scene import DOMAIN as SENSOR_DOMAIN, Scene +from homeassistant.const import CONF_PLATFORM +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TUYA, TuyaDevice +from . import TuyaDevice +from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}" PARALLEL_UPDATES = 0 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya scenes.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get("dev_ids") - devices = [] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tuya sensors dynamically through tuya discovery.""" + + platform = config_entry.data[CONF_PLATFORM] + + async def async_discover_sensor(dev_ids): + """Discover and add a discovered tuya sensor.""" + if not dev_ids: + return + entities = await hass.async_add_executor_job( + _setup_entities, hass, dev_ids, platform, + ) + async_add_entities(entities) + + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + ) + + devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) + await async_discover_sensor(devices_ids) + + +def _setup_entities(hass, dev_ids, platform): + """Set up Tuya Scene.""" + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: device = tuya.get_device_by_id(dev_id) if device is None: continue - devices.append(TuyaScene(device)) - add_entities(devices) + entities.append(TuyaScene(device, platform)) + return entities class TuyaScene(TuyaDevice, Scene): """Tuya Scene.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init Tuya scene.""" - super().__init__(tuya) + super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) def activate(self, **kwargs: Any) -> None: """Activate the scene.""" - self.tuya.activate() + self._tuya.activate() diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json new file mode 100644 index 00000000000..891eeb47643 --- /dev/null +++ b/homeassistant/components/tuya/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "flow_title": "Tuya configuration", + "step": { + "user": { + "title": "Tuya", + "description": "Enter your Tuya credential.", + "data": { + "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", + "password": "[%key:common::config_flow::data::password%]", + "platform": "The app where your account register", + "username": "[%key:common::config_flow::data::username%]" + } + } + }, + "abort": { + "auth_failed": "[%key:common::config_flow::error::invalid_auth%]", + "conn_error": "[%key:common::config_flow::error::cannot_connect%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "auth_failed": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 17cf5ae873a..13b4af94a48 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1,43 +1,69 @@ """Support for Tuya switches.""" -from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice +from homeassistant.components.switch import ( + DOMAIN as SENSOR_DOMAIN, + ENTITY_ID_FORMAT, + SwitchEntity, +) +from homeassistant.const import CONF_PLATFORM +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TUYA, TuyaDevice +from . import TuyaDevice +from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW PARALLEL_UPDATES = 0 -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up tuya sensors dynamically through tuya discovery.""" + + platform = config_entry.data[CONF_PLATFORM] + + async def async_discover_sensor(dev_ids): + """Discover and add a discovered tuya sensor.""" + if not dev_ids: + return + entities = await hass.async_add_executor_job( + _setup_entities, hass, dev_ids, platform, + ) + async_add_entities(entities) + + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + ) + + devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) + await async_discover_sensor(devices_ids) + + +def _setup_entities(hass, dev_ids, platform): """Set up Tuya Switch device.""" - if discovery_info is None: - return - tuya = hass.data[DATA_TUYA] - dev_ids = discovery_info.get("dev_ids") - devices = [] + tuya = hass.data[DOMAIN][TUYA_DATA] + entities = [] for dev_id in dev_ids: device = tuya.get_device_by_id(dev_id) if device is None: continue - devices.append(TuyaSwitch(device)) - add_entities(devices) + entities.append(TuyaSwitch(device, platform)) + return entities -class TuyaSwitch(TuyaDevice, SwitchDevice): +class TuyaSwitch(TuyaDevice, SwitchEntity): """Tuya Switch Device.""" - def __init__(self, tuya): + def __init__(self, tuya, platform): """Init Tuya switch device.""" - super().__init__(tuya) + super().__init__(tuya, platform) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) @property def is_on(self): """Return true if switch is on.""" - return self.tuya.state() + return self._tuya.state() def turn_on(self, **kwargs): """Turn the switch on.""" - self.tuya.turn_on() + self._tuya.turn_on() def turn_off(self, **kwargs): """Turn the device off.""" - self.tuya.turn_off() + self._tuya.turn_off() diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json new file mode 100644 index 00000000000..2cf09455b7b --- /dev/null +++ b/homeassistant/components/tuya/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_in_progress": "La configuraci\u00f3 de Tuya ja est\u00e0 en curs.", + "auth_failed": "Autenticaci\u00f3 inv\u00e0lida", + "conn_error": "No s'ha pogut connectar", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "auth_failed": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "Configuraci\u00f3 de Tuya", + "step": { + "user": { + "data": { + "country_code": "El teu codi de pa\u00eds (per exemple, 1 per l'EUA o 86 per la Xina)", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix la teva credencial de Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json new file mode 100644 index 00000000000..c3de7cbe020 --- /dev/null +++ b/homeassistant/components/tuya/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya configuration is already in progress.", + "auth_failed": "Invalid authentication", + "conn_error": "Failed to connect", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "auth_failed": "Invalid authentication" + }, + "flow_title": "Tuya configuration", + "step": { + "user": { + "data": { + "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", + "password": "Password", + "platform": "The app where your account register", + "username": "Username" + }, + "description": "Enter your Tuya credential.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json new file mode 100644 index 00000000000..163b2ed73c8 --- /dev/null +++ b/homeassistant/components/tuya/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "La configuraci\u00f3n de Tuya ya est\u00e1 en progreso.", + "auth_failed": "Autenticaci\u00f3n no v\u00e1lida", + "conn_error": "Fallo al conectarse", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "auth_failed": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "Configuraci\u00f3n Tuya", + "step": { + "user": { + "data": { + "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", + "password": "Contrase\u00f1a", + "platform": "La aplicaci\u00f3n en la cual registraste tu cuenta", + "username": "Usuario" + }, + "description": "Introduce tu credencial Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fi.json b/homeassistant/components/tuya/translations/fi.json new file mode 100644 index 00000000000..f0c2c5e71d3 --- /dev/null +++ b/homeassistant/components/tuya/translations/fi.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya-m\u00e4\u00e4ritykset ovat jo k\u00e4ynniss\u00e4.", + "conn_error": "Yhdist\u00e4minen ep\u00e4onnistui" + }, + "error": { + "auth_failed": "Virheellinen tunnistautuminen" + }, + "flow_title": "Tuya-asetukset", + "step": { + "user": { + "data": { + "country_code": "Tilisi maakoodi (esim. 1 Yhdysvalloissa, 358 Suomessa)", + "password": "Salasana", + "platform": "Sovellus, johon tili rekister\u00f6id\u00e4\u00e4n", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "description": "Anna Tuya-tunnistetietosi.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json new file mode 100644 index 00000000000..9860353504b --- /dev/null +++ b/homeassistant/components/tuya/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_in_progress": "La configuration de Tuya est d\u00e9j\u00e0 en cours." + }, + "flow_title": "Configuration Tuya", + "step": { + "user": { + "data": { + "platform": "L'application dans laquelle votre compte est enregistr\u00e9" + }, + "description": "Saisissez vos informations d'identification Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json new file mode 100644 index 00000000000..439a2442bad --- /dev/null +++ b/homeassistant/components/tuya/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "conn_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05d4 \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da (\u05dc\u05de\u05e9\u05dc, 1 \u05dc\u05d0\u05e8\u05d4\"\u05d1 \u05d0\u05d5 972 \u05dc\u05d9\u05e9\u05e8\u05d0\u05dc)", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "platform": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05d1\u05d4 \u05e8\u05e9\u05d5\u05dd \u05d7\u05e9\u05d1\u05d5\u05e0\u05da", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json new file mode 100644 index 00000000000..581121cc6f9 --- /dev/null +++ b/homeassistant/components/tuya/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "La configurazione di Tuya \u00e8 gi\u00e0 in corso.", + "auth_failed": "Autenticazione non valida", + "conn_error": "Impossibile connettersi", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "auth_failed": "Autenticazione non valida" + }, + "flow_title": "Configurazione di Tuya", + "step": { + "user": { + "data": { + "country_code": "Prefisso internazionale del tuo account (ad es. 1 per gli Stati Uniti o 86 per la Cina)", + "password": "Password", + "platform": "L'app in cui si registra il tuo account", + "username": "Nome utente" + }, + "description": "Inserisci le tue credenziali Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json new file mode 100644 index 00000000000..201e45ec357 --- /dev/null +++ b/homeassistant/components/tuya/translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "auth_failed": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "conn_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "auth_failed": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Tuya \uad6c\uc131\ud558\uae30", + "step": { + "user": { + "data": { + "country_code": "\uacc4\uc815 \uad6d\uac00 \ucf54\ub4dc (\uc608 : \ubbf8\uad6d\uc758 \uacbd\uc6b0 1, \uc911\uad6d\uc758 \uacbd\uc6b0 86)", + "password": "\ube44\ubc00\ubc88\ud638", + "platform": "\uacc4\uc815\uc774 \ub4f1\ub85d\ub41c \uc571", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Tuya \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json new file mode 100644 index 00000000000..d09427d71d8 --- /dev/null +++ b/homeassistant/components/tuya/translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya Konfiguratioun ass schonn am gaang.", + "auth_failed": "Ong\u00eblteg Authentifikatioun", + "conn_error": "Feeler beim verbannen", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + }, + "error": { + "auth_failed": "Ong\u00eblteg Authentifikatioun" + }, + "flow_title": "Tuya Konfiguratioun", + "step": { + "user": { + "data": { + "country_code": "De L\u00e4nner Code fir d\u00e4i Kont (beispill 1 fir USA oder 86 fir China)", + "password": "Passwuert", + "platform": "d'App wou den Kont registr\u00e9iert ass", + "username": "Benotzernumm" + }, + "description": "F\u00ebll deng Tuya Umeldungs Informatiounen aus.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json new file mode 100644 index 00000000000..253401e721e --- /dev/null +++ b/homeassistant/components/tuya/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya konfigurasjon er allerede i gang." + }, + "flow_title": "Tuya konfigurasjon", + "step": { + "user": { + "data": { + "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", + "platform": "Appen der kontoen din registreres" + }, + "description": "Skriv inn din Tuya-legitimasjon.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json new file mode 100644 index 00000000000..5f5f552d4c2 --- /dev/null +++ b/homeassistant/components/tuya/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "auth_failed": "[%key_id:common::config_flow::error::invalid_auth%]", + "conn_error": "[%key_id:common::config_flow::error::cannot_connect%]" + }, + "error": { + "auth_failed": "[%key_id:common::config_flow::error::invalid_auth%]" + }, + "step": { + "user": { + "data": { + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json new file mode 100644 index 00000000000..1452621b198 --- /dev/null +++ b/homeassistant/components/tuya/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "conn_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya", + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0412\u0430\u0448\u0435\u0433\u043e \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, 1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0412\u0430\u0448 \u0430\u043a\u043a\u0430\u0443\u043d\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json new file mode 100644 index 00000000000..762baa2af63 --- /dev/null +++ b/homeassistant/components/tuya/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "auth_failed": "Ogiltig anv\u00e4ndarinformation" + }, + "flow_title": "Tuya-konfiguration", + "step": { + "user": { + "data": { + "country_code": "Landskod f\u00f6r ditt konto (t.ex. 1 f\u00f6r USA eller 86 f\u00f6r Kina)", + "password": "L\u00f6senord", + "platform": "Appen d\u00e4r ditt konto registreras", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange dina Tuya anv\u00e4ndaruppgifter.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json new file mode 100644 index 00000000000..91c0936404d --- /dev/null +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_in_progress": "Tuya \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "auth_failed": "\u9a57\u8b49\u78bc\u7121\u6548", + "conn_error": "\u9023\u7dda\u5931\u6557", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "error": { + "auth_failed": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "Tuya \u8a2d\u5b9a", + "step": { + "user": { + "data": { + "country_code": "\u5e33\u865f\u570b\u5bb6\u4ee3\u78bc\uff08\u4f8b\u5982\uff1a\u7f8e\u570b 1 \u6216\u4e2d\u570b 86\uff09", + "password": "\u5bc6\u78bc", + "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165 Tuya \u6191\u8b49\u3002", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/es-419.json b/homeassistant/components/twentemilieu/translations/es-419.json index ed333bb9b51..6f190ba503c 100644 --- a/homeassistant/components/twentemilieu/translations/es-419.json +++ b/homeassistant/components/twentemilieu/translations/es-419.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "address_exists": "Direcci\u00f3n ya configurada." + }, + "error": { + "connection_error": "Error al conectar.", + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." + }, "step": { "user": { + "data": { + "house_letter": "Carta de la casa/adicional", + "house_number": "N\u00famero de casa", + "post_code": "C\u00f3digo postal" + }, + "description": "Configure Twente Milieu proporcionando informaci\u00f3n de recolecci\u00f3n de residuos en su direcci\u00f3n.", "title": "Twente Milieu" } } diff --git a/homeassistant/components/twentemilieu/translations/no.json b/homeassistant/components/twentemilieu/translations/no.json index 84fe87eda04..a9d3c184495 100644 --- a/homeassistant/components/twentemilieu/translations/no.json +++ b/homeassistant/components/twentemilieu/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "address_exists": "Adressen er allerede konfigurert." + "address_exists": "Adressen er allerede satt opp." }, "error": { "connection_error": "Tilkobling mislyktes.", @@ -15,7 +15,7 @@ "post_code": "Postnummer" }, "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.", - "title": "Twente Milieu" + "title": "" } } } diff --git a/homeassistant/components/twentemilieu/translations/pl.json b/homeassistant/components/twentemilieu/translations/pl.json index bfa38f9ef8a..e8cd67db9db 100644 --- a/homeassistant/components/twentemilieu/translations/pl.json +++ b/homeassistant/components/twentemilieu/translations/pl.json @@ -4,7 +4,7 @@ "address_exists": "Adres jest ju\u017c skonfigurowany." }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]", "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." }, "step": { diff --git a/homeassistant/components/twilio/translations/ko.json b/homeassistant/components/twilio/translations/ko.json index 63859a000d0..9ceca7c23dc 100644 --- a/homeassistant/components/twilio/translations/ko.json +++ b/homeassistant/components/twilio/translations/ko.json @@ -5,12 +5,12 @@ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio Webhook]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio \uc6f9 \ud6c5]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "description": "Twilio \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Twilio Webhook \uc124\uc815" + "title": "Twilio \uc6f9 \ud6c5 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index d25c52608e1..9f6d01cfb5d 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -88,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([UERadioDevice(session, player_id, player_name)]) -class UERadioDevice(MediaPlayerDevice): +class UERadioDevice(MediaPlayerEntity): """Representation of a Logitech UE Smart Radio device.""" def __init__(self, session, player_id, player_name): diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index e3225a2d210..610d39b0fb9 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -7,7 +7,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .config_flow import get_controller_id_from_config_entry -from .const import ATTR_MANUFACTURER, DOMAIN, LOGGER, UNIFI_WIRELESS_CLIENTS +from .const import ( + ATTR_MANUFACTURER, + DOMAIN as UNIFI_DOMAIN, + LOGGER, + UNIFI_WIRELESS_CLIENTS, +) from .controller import UniFiController SAVE_DELAY = 10 @@ -15,7 +20,8 @@ STORAGE_KEY = "unifi_data" STORAGE_VERSION = 1 CONFIG_SCHEMA = vol.Schema( - cv.deprecated(DOMAIN, invalidation_version="0.109"), {DOMAIN: cv.match_all} + cv.deprecated(UNIFI_DOMAIN, invalidation_version="0.109"), + {UNIFI_DOMAIN: cv.match_all}, ) @@ -29,16 +35,13 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up the UniFi component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + hass.data.setdefault(UNIFI_DOMAIN, {}) controller = UniFiController(hass, config_entry) - if not await controller.async_setup(): return False - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller + hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) @@ -62,8 +65,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - controller_id = get_controller_id_from_config_entry(config_entry) - controller = hass.data[DOMAIN].pop(controller_id) + controller = hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) return await controller.async_reset() @@ -90,15 +92,21 @@ class UnifiWirelessClients: def get_data(self, config_entry): """Get data related to a specific controller.""" controller_id = get_controller_id_from_config_entry(config_entry) - data = self.data.get(controller_id, {"wireless_devices": []}) + key = config_entry.entry_id + if controller_id in self.data: + key = controller_id + + data = self.data.get(key, {"wireless_devices": []}) return set(data["wireless_devices"]) @callback def update_data(self, data, config_entry): """Update data and schedule to save to file.""" controller_id = get_controller_id_from_config_entry(config_entry) - self.data[controller_id] = {"wireless_devices": list(data)} + if controller_id in self.data: + self.data.pop(controller_id) + self.data[config_entry.entry_id] = {"wireless_devices": list(data)} self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index f584a41ae54..6115821b000 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -28,16 +28,11 @@ from .const import ( CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, DEFAULT_POE_CLIENTS, - DOMAIN, + DOMAIN as UNIFI_DOMAIN, LOGGER, ) from .controller import get_controller -from .errors import ( - AlreadyConfigured, - AuthenticationRequired, - CannotConnect, - NoLocalUser, -) +from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect DEFAULT_PORT = 8443 DEFAULT_SITE_ID = "default" @@ -53,13 +48,7 @@ def get_controller_id_from_config_entry(config_entry): ) -@callback -def get_controller_from_config_entry(hass, config_entry): - """Return controller with a matching bridge id.""" - return hass.data[DOMAIN][get_controller_id_from_config_entry(config_entry)] - - -class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): """Handle a UniFi config flow.""" VERSION = 1 @@ -140,8 +129,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for site in self.sites.values(): if desc == site["desc"]: - if "role" not in site: - raise NoLocalUser self.config[CONF_SITE_ID] = site["name"] break @@ -160,9 +147,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except AlreadyConfigured: return self.async_abort(reason="already_configured") - except NoLocalUser: - return self.async_abort(reason="no_local_user") - if len(self.sites) == 1: self.desc = next(iter(self.sites.values()))["desc"] return await self.async_step_site(user_input={}) @@ -189,9 +173,45 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the UniFi options.""" - self.controller = get_controller_from_config_entry(self.hass, self.config_entry) + self.controller = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id] self.options[CONF_BLOCK_CLIENT] = self.controller.option_block_clients - return await self.async_step_device_tracker() + + if self.show_advanced_options: + return await self.async_step_device_tracker() + + return await self.async_step_simple_options() + + async def async_step_simple_options(self, user_input=None): + """For simple Jack.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + clients_to_block = {} + + for client in self.controller.api.clients.values(): + clients_to_block[ + client.mac + ] = f"{client.name or client.hostname} ({client.mac})" + + return self.async_show_form( + step_id="simple_options", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TRACK_CLIENTS, + default=self.controller.option_track_clients, + ): bool, + vol.Optional( + CONF_TRACK_DEVICES, + default=self.controller.option_track_devices, + ): bool, + vol.Optional( + CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT] + ): cv.multi_select(clients_to_block), + } + ), + ) async def async_step_device_tracker(self, user_input=None): """Manage the device tracker options.""" @@ -209,7 +229,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): | { wlan["name"] for ap in self.controller.api.devices.values() - for wlan in ap.raw.get("wlan_overrides", []) + for wlan in ap.wlan_overrides + if "name" in wlan } ) ssid_filter = {ssid: ssid for ssid in sorted(list(ssids))} diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 2500d6b4106..82314adb771 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -6,18 +6,23 @@ import ssl from aiohttp import CookieJar import aiounifi from aiounifi.controller import ( - DATA_CLIENT, DATA_CLIENT_REMOVED, - DATA_DEVICE, DATA_EVENT, SIGNAL_CONNECTION_STATE, SIGNAL_DATA, ) -from aiounifi.events import WIRELESS_CLIENT_CONNECTED, WIRELESS_GUEST_CONNECTED +from aiounifi.events import ( + ACCESS_POINT_CONNECTED, + GATEWAY_CONNECTED, + SWITCH_CONNECTED, + WIRED_CLIENT_CONNECTED, + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, +) from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout -from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN +from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import CONF_HOST @@ -46,14 +51,25 @@ from .const import ( DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DOMAIN, + DOMAIN as UNIFI_DOMAIN, LOGGER, UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 -SUPPORTED_PLATFORMS = [DT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] +SUPPORTED_PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] + +CLIENT_CONNECTED = ( + WIRED_CLIENT_CONNECTED, + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, +) +DEVICE_CONNECTED = ( + ACCESS_POINT_CONNECTED, + GATEWAY_CONNECTED, + SWITCH_CONNECTED, +) class UniFiController: @@ -174,7 +190,7 @@ class UniFiController: if signal == SIGNAL_CONNECTION_STATE: if data == STATE_DISCONNECTED and self.available: - LOGGER.error("Lost connection to UniFi") + LOGGER.warning("Lost connection to UniFi controller") if (data == STATE_RUNNING and not self.available) or ( data == STATE_DISCONNECTED and self.available @@ -183,19 +199,40 @@ class UniFiController: async_dispatcher_send(self.hass, self.signal_reachable) if not self.available: - self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) + else: + LOGGER.info("Connected to UniFi controller") elif signal == SIGNAL_DATA and data: if DATA_EVENT in data: - if next(iter(data[DATA_EVENT])).event in ( - WIRELESS_CLIENT_CONNECTED, - WIRELESS_GUEST_CONNECTED, - ): - self.update_wireless_clients() + clients_connected = set() + devices_connected = set() + wireless_clients_connected = False - elif DATA_CLIENT in data or DATA_DEVICE in data: - async_dispatcher_send(self.hass, self.signal_update) + for event in data[DATA_EVENT]: + + if event.event in CLIENT_CONNECTED: + clients_connected.add(event.mac) + + if not wireless_clients_connected and event.event in ( + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, + ): + wireless_clients_connected = True + + elif event.event in DEVICE_CONNECTED: + devices_connected.add(event.mac) + + if wireless_clients_connected: + self.update_wireless_clients() + if clients_connected or devices_connected: + async_dispatcher_send( + self.hass, + self.signal_update, + clients_connected, + devices_connected, + ) elif DATA_CLIENT_REMOVED in data: async_dispatcher_send( @@ -253,9 +290,11 @@ class UniFiController: for site in sites.values(): if self.site == site["name"]: self._site_name = site["desc"] - self._site_role = site["role"] break + description = await self.api.site_description() + self._site_role = description[0]["site_role"] + except CannotConnect: raise ConfigEntryNotReady @@ -263,6 +302,30 @@ class UniFiController: LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False + # Restore clients that is not a part of active clients list. + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + for entity in entity_registry.entities.values(): + if ( + entity.config_entry_id != self.config_entry.entry_id + or "-" not in entity.unique_id + ): + continue + + mac = "" + if entity.domain == TRACKER_DOMAIN: + mac, _ = entity.unique_id.split("-", 1) + elif entity.domain == SWITCH_DOMAIN: + _, mac = entity.unique_id.split("-", 1) + + if mac in self.api.clients or mac not in self.api.clients_all: + continue + + client = self.api.clients_all[mac] + self.api.clients.process_raw([client.raw]) + LOGGER.debug( + "Restore disconnected client %s (%s)", entity.entity_id, client.mac, + ) + wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() @@ -281,20 +344,16 @@ class UniFiController: return True @staticmethod - async def async_config_entry_updated(hass, entry) -> None: + async def async_config_entry_updated(hass, config_entry) -> None: """Handle signals of config entry being updated.""" - controller_id = CONTROLLER_ID.format( - host=entry.data[CONF_CONTROLLER][CONF_HOST], - site=entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) - controller = hass.data[DOMAIN][controller_id] - + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] async_dispatcher_send(hass, controller.signal_options_update) @callback - def reconnect(self) -> None: + def reconnect(self, log=False) -> None: """Prepare to reconnect UniFi session.""" - LOGGER.debug("Reconnecting to UniFi in %i", RETRY_TIMER) + if log: + LOGGER.info("Will try to reconnect to UniFi controller") self.hass.loop.create_task(self.async_reconnect()) async def async_reconnect(self) -> None: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b56fe117ef7..ebad63acb4e 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,22 +1,36 @@ """Track devices using UniFi controllers.""" +from datetime import timedelta import logging +from aiounifi.api import SOURCE_DATA, SOURCE_EVENT +from aiounifi.events import ( + ACCESS_POINT_UPGRADED, + GATEWAY_UPGRADED, + SWITCH_UPGRADED, + WIRED_CLIENT_CONNECTED, + WIRELESS_CLIENT_CONNECTED, + WIRELESS_CLIENT_ROAM, + WIRELESS_CLIENT_ROAMRADIO, +) + from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry -from homeassistant.components.unifi.unifi_entity_base import UniFiBase from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util -from .const import ATTR_MANUFACTURER +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .unifi_client import UniFiClient +from .unifi_entity_base import UniFiBase LOGGER = logging.getLogger(__name__) +CLIENT_TRACKER = "client" +DEVICE_TRACKER = "device" + CLIENT_CONNECTED_ATTRIBUTES = [ "_is_guest_by_uap", "ap_mac", @@ -39,40 +53,31 @@ CLIENT_STATIC_ATTRIBUTES = [ "oui", ] -CLIENT_TRACKER = "client" -DEVICE_TRACKER = "device" +DEVICE_UPGRADED = (ACCESS_POINT_UPGRADED, GATEWAY_UPGRADED, SWITCH_UPGRADED) + +WIRED_CONNECTION = (WIRED_CLIENT_CONNECTED,) +WIRELESS_CONNECTION = ( + WIRELESS_CLIENT_CONNECTED, + WIRELESS_CLIENT_ROAM, + WIRELESS_CLIENT_ROAMRADIO, +) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for UniFi component.""" - controller = get_controller_from_config_entry(hass, config_entry) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} - # Restore clients that is not a part of active clients list. - entity_registry = await hass.helpers.entity_registry.async_get_registry() - for entity in entity_registry.entities.values(): - - if ( - entity.config_entry_id == config_entry.entry_id - and entity.domain == DOMAIN - and "-" in entity.unique_id - ): - - mac, _ = entity.unique_id.split("-", 1) - if mac in controller.api.clients or mac not in controller.api.clients_all: - continue - - client = controller.api.clients_all[mac] - controller.api.clients.process_raw([client.raw]) - LOGGER.debug( - "Restore disconnected client %s (%s)", entity.entity_id, client.mac, - ) - @callback - def items_added(): + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: """Update the values of the controller.""" - if controller.option_track_clients or controller.option_track_devices: - add_entities(controller, async_add_entities) + if controller.option_track_clients: + add_client_entities(controller, async_add_entities, clients) + + if controller.option_track_devices: + add_device_entities(controller, async_add_entities, devices) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -81,38 +86,43 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def add_entities(controller, async_add_entities): - """Add new tracker entities from the controller.""" +def add_client_entities(controller, async_add_entities, clients): + """Add new client tracker entities from the controller.""" trackers = [] - for items, tracker_class, track in ( - (controller.api.clients, UniFiClientTracker, controller.option_track_clients), - (controller.api.devices, UniFiDeviceTracker, controller.option_track_devices), - ): - if not track: + for mac in clients: + if mac in controller.entities[DOMAIN][UniFiClientTracker.TYPE]: continue - for mac in items: + client = controller.api.clients[mac] - if mac in controller.entities[DOMAIN][tracker_class.TYPE]: + if mac not in controller.wireless_clients: + if not controller.option_track_wired_clients: continue + elif ( + client.essid + and controller.option_ssid_filter + and client.essid not in controller.option_ssid_filter + ): + continue - item = items[mac] + trackers.append(UniFiClientTracker(client, controller)) - if tracker_class is UniFiClientTracker: + if trackers: + async_add_entities(trackers) - if mac not in controller.wireless_clients: - if not controller.option_track_wired_clients: - continue - else: - if ( - item.essid - and controller.option_ssid_filter - and item.essid not in controller.option_ssid_filter - ): - continue - trackers.append(tracker_class(item, controller)) +@callback +def add_device_entities(controller, async_add_entities, devices): + """Add new device tracker entities from the controller.""" + trackers = [] + + for mac in devices: + if mac in controller.entities[DOMAIN][UniFiDeviceTracker.TYPE]: + continue + + device = controller.api.devices[mac] + trackers.append(UniFiDeviceTracker(device, controller)) if trackers: async_add_entities(trackers) @@ -128,78 +138,83 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): """Set up tracked client.""" super().__init__(client, controller) + self.schedule_update = False self.cancel_scheduled_update = None - self.is_disconnected = None - self.wired_bug = None - if self.is_wired != self.client.is_wired: - self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time + self._is_connected = False + if client.last_seen: + self._is_connected = ( + self.is_wired == client.is_wired + and dt_util.utcnow() + - dt_util.utc_from_timestamp(float(client.last_seen)) + < controller.option_detection_time + ) + if self._is_connected: + self.schedule_update = True - @property - def is_connected(self): - """Return true if the client is connected to the network. + async def async_will_remove_from_hass(self) -> None: + """Disconnect object when removed.""" + if self.cancel_scheduled_update: + self.cancel_scheduled_update() + await super().async_will_remove_from_hass() - If connected to unwanted ssid return False. - If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. - """ + @callback + def async_update_callback(self) -> None: + """Update the clients state.""" @callback - def _scheduled_update(now): - """Scheduled callback for update.""" - self.is_disconnected = True + def _make_disconnected(now): + """Mark client as disconnected.""" + self._is_connected = False self.cancel_scheduled_update = None self.async_write_ha_state() - if (self.is_wired and self.wired_connection) or ( - not self.is_wired and self.wireless_connection - ): + if self.client.last_updated == SOURCE_EVENT: + + if (self.is_wired and self.client.event.event in WIRED_CONNECTION) or ( + not self.is_wired and self.client.event.event in WIRELESS_CONNECTION + ): + self._is_connected = True + self.schedule_update = False + if self.cancel_scheduled_update: + self.cancel_scheduled_update() + self.cancel_scheduled_update = None + + # Ignore extra scheduled update from wired bug + elif not self.cancel_scheduled_update: + self.schedule_update = True + + elif not self.client.event and self.client.last_updated == SOURCE_DATA: + + if self.is_wired == self.client.is_wired: + self._is_connected = True + self.schedule_update = True + + if self.schedule_update: + self.schedule_update = False + if self.cancel_scheduled_update: self.cancel_scheduled_update() - self.cancel_scheduled_update = None - self.is_disconnected = False + self.cancel_scheduled_update = async_track_point_in_utc_time( + self.hass, + _make_disconnected, + dt_util.utcnow() + self.controller.option_detection_time, + ) - if (self.is_wired and self.wired_connection is False) or ( - not self.is_wired and self.wireless_connection is False - ): - if not self.is_disconnected and not self.cancel_scheduled_update: - self.cancel_scheduled_update = async_track_point_in_utc_time( - self.hass, - _scheduled_update, - dt_util.utcnow() + self.controller.option_detection_time, - ) + super().async_update_callback() + @property + def is_connected(self): + """Return true if the client is connected to the network.""" if ( not self.is_wired and self.client.essid and self.controller.option_ssid_filter and self.client.essid not in self.controller.option_ssid_filter - and not self.cancel_scheduled_update ): return False - if self.is_disconnected is not None: - return not self.is_disconnected - - if self.is_wired != self.client.is_wired: - if not self.wired_bug: - self.wired_bug = dt_util.utcnow() - since_last_seen = dt_util.utcnow() - self.wired_bug - - else: - self.wired_bug = None - - # A client that has never been seen cannot be connected. - if self.client.last_seen is None: - return False - - since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( - float(self.client.last_seen) - ) - - if since_last_seen < self.controller.option_detection_time: - return True - - return False + return self._is_connected @property def source_type(self): @@ -220,7 +235,7 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): for variable in CLIENT_STATIC_ATTRIBUTES + CLIENT_CONNECTED_ATTRIBUTES: if variable in self.client.raw: - if self.is_disconnected and variable in CLIENT_CONNECTED_ATTRIBUTES: + if not self.is_connected and variable in CLIENT_CONNECTED_ATTRIBUTES: continue attributes[variable] = self.client.raw[variable] @@ -229,17 +244,17 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_track_clients: - await self.async_remove() + await self.remove_item({self.client.mac}) elif self.is_wired: if not self.controller.option_track_wired_clients: - await self.async_remove() - else: - if ( - self.controller.option_ssid_filter - and self.client.essid not in self.controller.option_ssid_filter - ): - await self.async_remove() + await self.remove_item({self.client.mac}) + + elif ( + self.controller.option_ssid_filter + and self.client.essid not in self.controller.option_ssid_filter + ): + await self.remove_item({self.client.mac}) class UniFiDeviceTracker(UniFiBase, ScannerEntity): @@ -250,41 +265,58 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): def __init__(self, device, controller): """Set up tracked device.""" - self.device = device - super().__init__(controller) + super().__init__(device, controller) + + self._is_connected = device.state == 1 + self.cancel_scheduled_update = None @property - def mac(self): - """Return MAC of device.""" - return self.device.mac - - async def async_added_to_hass(self): - """Subscribe to device events.""" - await super().async_added_to_hass() - LOGGER.debug("New device %s (%s)", self.entity_id, self.device.mac) - self.device.register_callback(self.async_update_callback) + def device(self): + """Wrap item.""" + return self._item async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" + if self.cancel_scheduled_update: + self.cancel_scheduled_update() await super().async_will_remove_from_hass() - self.device.remove_callback(self.async_update_callback) @callback def async_update_callback(self): - """Update the sensor's state.""" - LOGGER.debug("Updating device %s (%s)", self.entity_id, self.device.mac) - self.async_write_ha_state() + """Update the devices' state.""" + + @callback + def _no_heartbeat(now): + """No heart beat by device.""" + self._is_connected = False + self.cancel_scheduled_update = None + self.async_write_ha_state() + + if self.device.last_updated == SOURCE_DATA: + self._is_connected = True + + if self.cancel_scheduled_update: + self.cancel_scheduled_update() + + self.cancel_scheduled_update = async_track_point_in_utc_time( + self.hass, + _no_heartbeat, + dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 10), + ) + + elif ( + self.device.last_updated == SOURCE_EVENT + and self.device.event.event in DEVICE_UPGRADED + ): + self.hass.async_create_task(self.async_update_device_registry()) + return + + super().async_update_callback() @property def is_connected(self): """Return true if the device is connected to the network.""" - if self.device.state == 1 and ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) - < self.controller.option_detection_time - ): - return True - - return False + return self._is_connected @property def source_type(self): @@ -321,6 +353,14 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): return info + async def async_update_device_registry(self) -> None: + """Update device registry.""" + device_registry = await self.hass.helpers.device_registry.async_get_registry() + + device_registry.async_get_or_create( + config_entry_id=self.controller.config_entry.entry_id, **self.device_info + ) + @property def device_state_attributes(self): """Return the device state attributes.""" @@ -343,4 +383,4 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_track_devices: - await self.async_remove() + await self.remove_item({self.device.mac}) diff --git a/homeassistant/components/unifi/errors.py b/homeassistant/components/unifi/errors.py index e0da64f245c..c90c4956312 100644 --- a/homeassistant/components/unifi/errors.py +++ b/homeassistant/components/unifi/errors.py @@ -22,9 +22,5 @@ class LoginRequired(UnifiException): """Component got logged out.""" -class NoLocalUser(UnifiException): - """No local user.""" - - class UserLevel(UnifiException): """User level too low.""" diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 8c05d195316..124c7241c30 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==20"], - "codeowners": ["@kane610"], + "requirements": ["aiounifi==22"], + "codeowners": ["@Kane610"], "quality_scale": "platinum" } diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 964db5820b8..8fdb0ac1461 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -2,11 +2,11 @@ import logging from homeassistant.components.sensor import DOMAIN -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import DOMAIN as UNIFI_DOMAIN from .unifi_client import UniFiClient LOGGER = logging.getLogger(__name__) @@ -21,14 +21,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for UniFi integration.""" - controller = get_controller_from_config_entry(hass, config_entry) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {RX_SENSOR: set(), TX_SENSOR: set()} @callback - def items_added(): + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: """Update the values of the controller.""" if controller.option_allow_bandwidth_sensors: - add_entities(controller, async_add_entities) + add_entities(controller, async_add_entities, clients) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -37,14 +39,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def add_entities(controller, async_add_entities): +def add_entities(controller, async_add_entities, clients): """Add new sensor entities from the controller.""" sensors = [] - for mac in controller.api.clients: + for mac in clients: for sensor_class in (UniFiRxBandwidthSensor, UniFiTxBandwidthSensor): - if mac not in controller.entities[DOMAIN][sensor_class.TYPE]: - sensors.append(sensor_class(controller.api.clients[mac], controller)) + if mac in controller.entities[DOMAIN][sensor_class.TYPE]: + continue + + client = controller.api.clients[mac] + sensors.append(sensor_class(client, controller)) if sensors: async_add_entities(sensors) @@ -68,7 +73,7 @@ class UniFiBandwidthSensor(UniFiClient): async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_allow_bandwidth_sensors: - await self.async_remove() + await self.remove_item({self.client.mac}) class UniFiRxBandwidthSensor(UniFiBandwidthSensor): diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index df7fc267a43..df1cb753b53 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -4,24 +4,22 @@ "user": { "title": "Set up UniFi Controller", "data": { - "host": "Host", - "username": "User name", - "password": "Password", - "port": "Port", + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", "site": "Site ID", "verify_ssl": "Controller using proper certificate" } } }, "error": { - "faulty_credentials": "Bad user credentials", - "service_unavailable": "No service available", + "faulty_credentials": "[%key:common::config_flow::error::invalid_auth%]", + "service_unavailable": "[%key:common::config_flow::error::cannot_connect%]", "unknown_client_mac": "No client available on that MAC address" }, "abort": { - "already_configured": "Controller site is already configured", - "no_local_user": "No local user found, configure a local account on controller and try again", - "user_privilege": "User needs to be administrator" + "already_configured": "Controller site is already configured" } }, "options": { @@ -47,6 +45,14 @@ "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", "title": "UniFi options 2/3" }, + "simple_options": { + "data": { + "track_clients": "[%key:component::unifi::options::step::device_tracker::data::track_clients%]", + "track_devices": "[%key:component::unifi::options::step::device_tracker::data::track_devices%]", + "block_client": "[%key:component::unifi::options::step::client_control::data::block_client%]" + }, + "description": "Configure UniFi integration" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 5fb6daf524c..1b9f7147a9a 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,12 +1,20 @@ """Support for devices connected to UniFi POE.""" import logging -from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry +from aiounifi.api import SOURCE_EVENT +from aiounifi.events import ( + WIRED_CLIENT_BLOCKED, + WIRED_CLIENT_UNBLOCKED, + WIRELESS_CLIENT_BLOCKED, + WIRELESS_CLIENT_UNBLOCKED, +) + +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +from .const import DOMAIN as UNIFI_DOMAIN from .unifi_client import UniFiClient LOGGER = logging.getLogger(__name__) @@ -14,6 +22,9 @@ LOGGER = logging.getLogger(__name__) BLOCK_SWITCH = "block" POE_SWITCH = "poe" +CLIENT_BLOCKED = (WIRED_CLIENT_BLOCKED, WIRELESS_CLIENT_BLOCKED) +CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Component doesn't support configuration through configuration.yaml.""" @@ -24,118 +35,119 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Switches are controlling network access and switch ports with POE. """ - controller = get_controller_from_config_entry(hass, config_entry) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {BLOCK_SWITCH: set(), POE_SWITCH: set()} if controller.site_role != "admin": return - switches_off = [] - - # Restore clients that is not a part of active clients list. + # Store previously known POE control entities in case their POE are turned off. + previously_known_poe_clients = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() for entity in entity_registry.entities.values(): if ( - entity.config_entry_id == config_entry.entry_id - and entity.unique_id.startswith(f"{POE_SWITCH}-") + entity.config_entry_id != config_entry.entry_id + or not entity.unique_id.startswith(POE_SWITCH) ): + continue - _, mac = entity.unique_id.split("-", 1) + mac = entity.unique_id.replace(f"{POE_SWITCH}-", "") + if mac in controller.api.clients or mac in controller.api.clients_all: + previously_known_poe_clients.append(entity.unique_id) - if mac in controller.api.clients: - switches_off.append(entity.unique_id) - continue - - if mac in controller.api.clients_all: - client = controller.api.clients_all[mac] - controller.api.clients.process_raw([client.raw]) - switches_off.append(entity.unique_id) - continue + for mac in controller.option_block_clients: + if mac not in controller.api.clients and mac in controller.api.clients_all: + client = controller.api.clients_all[mac] + controller.api.clients.process_raw([client.raw]) @callback - def items_added(): + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: """Update the values of the controller.""" - if controller.option_block_clients or controller.option_poe_clients: - add_entities(controller, async_add_entities, switches_off) + if controller.option_block_clients: + add_block_entities(controller, async_add_entities, clients) + + if controller.option_poe_clients: + add_poe_entities( + controller, async_add_entities, clients, previously_known_poe_clients + ) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) items_added() - switches_off.clear() + previously_known_poe_clients.clear() @callback -def add_entities(controller, async_add_entities, switches_off): +def add_block_entities(controller, async_add_entities, clients): """Add new switch entities from the controller.""" switches = [] for mac in controller.option_block_clients: - - if mac in controller.entities[DOMAIN][BLOCK_SWITCH]: - continue - - client = None - - if mac in controller.api.clients: - client = controller.api.clients[mac] - - elif mac in controller.api.clients_all: - client = controller.api.clients_all[mac] - - if not client: + if mac in controller.entities[DOMAIN][BLOCK_SWITCH] or mac not in clients: continue + client = controller.api.clients[mac] switches.append(UniFiBlockClientSwitch(client, controller)) - if controller.option_poe_clients: - devices = controller.api.devices - - for mac in controller.api.clients: - - poe_client_id = f"{POE_SWITCH}-{mac}" - - if mac in controller.entities[DOMAIN][POE_SWITCH]: - continue - - client = controller.api.clients[mac] - - if poe_client_id not in switches_off and ( - mac in controller.wireless_clients - or client.sw_mac not in devices - or not devices[client.sw_mac].ports[client.sw_port].port_poe - or not devices[client.sw_mac].ports[client.sw_port].poe_enable - or controller.mac == client.mac - ): - continue - - # Multiple POE-devices on same port means non UniFi POE driven switch - multi_clients_on_port = False - for client2 in controller.api.clients.values(): - - if poe_client_id in switches_off: - break - - if ( - client2.is_wired - and client.mac != client2.mac - and client.sw_mac == client2.sw_mac - and client.sw_port == client2.sw_port - ): - multi_clients_on_port = True - break - - if multi_clients_on_port: - continue - - switches.append(UniFiPOEClientSwitch(client, controller)) - if switches: async_add_entities(switches) -class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): +@callback +def add_poe_entities( + controller, async_add_entities, clients, previously_known_poe_clients +): + """Add new switch entities from the controller.""" + switches = [] + + devices = controller.api.devices + + for mac in clients: + if mac in controller.entities[DOMAIN][POE_SWITCH]: + continue + + poe_client_id = f"{POE_SWITCH}-{mac}" + client = controller.api.clients[mac] + + if poe_client_id not in previously_known_poe_clients and ( + mac in controller.wireless_clients + or client.sw_mac not in devices + or not devices[client.sw_mac].ports[client.sw_port].port_poe + or not devices[client.sw_mac].ports[client.sw_port].poe_enable + or controller.mac == client.mac + ): + continue + + # Multiple POE-devices on same port means non UniFi POE driven switch + multi_clients_on_port = False + for client2 in controller.api.clients.values(): + + if poe_client_id in previously_known_poe_clients: + break + + if ( + client2.is_wired + and client.mac != client2.mac + and client.sw_mac == client2.sw_mac + and client.sw_port == client2.sw_port + ): + multi_clients_on_port = True + break + + if multi_clients_on_port: + continue + + switches.append(UniFiPOEClientSwitch(client, controller)) + + if switches: + async_add_entities(switches) + + +class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): """Representation of a client that uses POE.""" DOMAIN = DOMAIN @@ -146,7 +158,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): super().__init__(client, controller) self.poe_mode = None - if self.client.sw_port and self.port.poe_mode != "off": + if client.sw_port and self.port.poe_mode != "off": self.poe_mode = self.port.poe_mode async def async_added_to_hass(self): @@ -227,19 +239,35 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_poe_clients: - await self.async_remove() + await self.remove_item({self.client.mac}) -class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): +class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): """Representation of a blockable client.""" DOMAIN = DOMAIN TYPE = BLOCK_SWITCH + def __init__(self, client, controller): + """Set up block switch.""" + super().__init__(client, controller) + + self._is_blocked = client.blocked + + @callback + def async_update_callback(self) -> None: + """Update the clients state.""" + if self.client.last_updated == SOURCE_EVENT: + + if self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED: + self._is_blocked = self.client.event.event in CLIENT_BLOCKED + + super().async_update_callback() + @property def is_on(self): """Return true if client is allowed to connect.""" - return not self.is_blocked + return not self._is_blocked async def async_turn_on(self, **kwargs): """Turn on connectivity for client.""" @@ -252,11 +280,11 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): @property def icon(self): """Return the icon to use in the frontend.""" - if self.is_blocked: + if self._is_blocked: return "mdi:network-off" return "mdi:network" async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if self.client.mac not in self.controller.option_block_clients: - await self.async_remove() + await self.remove_item({self.client.mac}) diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index 6d8b395f2b0..0fdbf0cde10 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -2,21 +2,22 @@ "config": { "abort": { "already_configured": "El lloc del controlador ja est\u00e0 configurat", + "no_local_user": "No s'ha trobat cap usuari local, configura un compte local al controlador i torna-ho a provar", "user_privilege": "L'usuari ha de ser administrador" }, "error": { - "faulty_credentials": "Credencials d'usuari incorrectes", - "service_unavailable": "Servei no disponible", + "faulty_credentials": "[%key::common::config_flow::error::invalid_auth%]", + "service_unavailable": "[%key::common::config_flow::error::cannot_connect%]", "unknown_client_mac": "No hi ha cap client disponible en aquesta adre\u00e7a MAC" }, "step": { "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%]", "site": "ID del lloc", - "username": "Nom d'usuari", + "username": "[%key::common::config_flow::data::username%]", "verify_ssl": "El controlador est\u00e0 utilitzant un certificat adequat" }, "title": "Configuraci\u00f3 del controlador UniFi" @@ -28,7 +29,7 @@ "client_control": { "data": { "block_client": "Clients controlats amb acc\u00e9s a la xarxa", - "new_client": "Afegeix un client nou per al control d\u2019acc\u00e9s a la xarxa", + "new_client": "Afegeix un client nou per al control d'acc\u00e9s a la xarxa", "poe_clients": "Permet control POE dels clients" }, "description": "Configura els controls del client \n\nConfigura interruptors per als n\u00fameros de s\u00e8rie als quals vulguis controlar l'acc\u00e9s a la xarxa.", @@ -45,11 +46,19 @@ "description": "Configuraci\u00f3 de seguiment de dispositius", "title": "Opcions d'UniFi" }, + "simple_options": { + "data": { + "block_client": "[%key::component::unifi::options::step::client_control::data::block_client%]", + "track_clients": "[%key::component::unifi::options::step::device_tracker::data::track_clients%]", + "track_devices": "[%key::component::unifi::options::step::device_tracker::data::track_devices%]" + }, + "description": "Configura la integraci\u00f3 d'UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" }, - "description": "Configuraci\u00f3 dels sensors d\u2019estad\u00edstiques", + "description": "Configuraci\u00f3 dels sensors d'estad\u00edstiques", "title": "Opcions d'UniFi" } } diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index c28ca26919c..8fe1c060992 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -24,6 +24,13 @@ }, "options": { "step": { + "simple_options": { + "data": { + "track_clients": "Sledov\u00e1n\u00ed p\u0159ipojen\u00fdch za\u0159\u00edzen\u00ed", + "track_devices": "Sledov\u00e1n\u00ed s\u00ed\u0165ov\u00fdch za\u0159\u00edzen\u00ed (za\u0159\u00edzen\u00ed Ubiquiti)" + }, + "description": "Konfigurace integrace UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro s\u00ed\u0165ov\u00e9 klienty" diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 0683f3da594..4ef34b3915b 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Controller-Site ist bereits konfiguriert", + "no_local_user": "Kein lokaler Benutzer gefunden, konfigurieren Sie ein lokales Konto auf dem Controller und versuchen Sie es erneut", "user_privilege": "Der Benutzer muss Administrator sein" }, "error": { @@ -51,6 +52,12 @@ "other": "andere" } }, + "simple_options": { + "data": { + "track_clients": "Netzwerk Ger\u00e4te \u00fcberwachen" + }, + "description": "Konfigurieren Sie die UniFi-Integration" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients" diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index d61dba3b5ef..279904ebd95 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -6,8 +6,8 @@ "user_privilege": "User needs to be administrator" }, "error": { - "faulty_credentials": "Bad user credentials", - "service_unavailable": "No service available", + "faulty_credentials": "Invalid authentication", + "service_unavailable": "Failed to connect", "unknown_client_mac": "No client available on that MAC address" }, "step": { @@ -17,7 +17,7 @@ "password": "Password", "port": "Port", "site": "Site ID", - "username": "User name", + "username": "Username", "verify_ssl": "Controller using proper certificate" }, "title": "Set up UniFi Controller" @@ -47,6 +47,14 @@ "description": "Configure device tracking", "title": "UniFi options 1/3" }, + "simple_options": { + "data": { + "block_client": "Network access controlled clients", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)" + }, + "description": "Configure UniFi integration" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" diff --git a/homeassistant/components/unifi/translations/es-419.json b/homeassistant/components/unifi/translations/es-419.json index ac50051b6c8..7f92a0b45af 100644 --- a/homeassistant/components/unifi/translations/es-419.json +++ b/homeassistant/components/unifi/translations/es-419.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Credenciales de usuario incorrectas", - "service_unavailable": "No hay servicio disponible" + "service_unavailable": "No hay servicio disponible", + "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, "step": { "user": { @@ -21,5 +22,45 @@ "title": "Configurar el controlador UniFi" } } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "Acceso controlado a la red de clientes", + "new_client": "Agregar nuevo cliente para control de acceso a la red", + "poe_clients": "Permitir control POE de clientes" + }, + "description": "Configurar controles de cliente \n\nCree conmutadores para los n\u00fameros de serie para los que desea controlar el acceso a la red.", + "title": "Opciones UniFi 2/3" + }, + "device_tracker": { + "data": { + "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta que se consider\u00f3", + "ignore_wired_bug": "Deshabilitar la l\u00f3gica de error con cable UniFi", + "ssid_filter": "Seleccione SSID para rastrear clientes inal\u00e1mbricos en", + "track_clients": "Rastree clientes de red", + "track_devices": "Dispositivos de red de seguimiento (dispositivos Ubiquiti)", + "track_wired_clients": "Incluir clientes de red cableada" + }, + "description": "Configurar el seguimiento del dispositivo", + "title": "Opciones UniFi 1/3" + }, + "simple_options": { + "data": { + "block_client": "Acceso controlado a la red de clientes", + "track_clients": "Rastree clientes en la red", + "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)" + }, + "description": "Configurar la integraci\u00f3n de UniFi" + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para clientes de red" + }, + "description": "Configurar sensores de estad\u00edsticas", + "title": "Opciones UniFi 3/3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index dbced017d77..1d2bb2977cb 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "no_local_user": "No se encontr\u00f3 ning\u00fan usuario local, configure una cuenta local en el controlador e int\u00e9ntelo de nuevo", "user_privilege": "El usuario debe ser administrador" }, "error": { - "faulty_credentials": "Credenciales de usuario incorrectas", - "service_unavailable": "Servicio No disponible", + "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", + "service_unavailable": "Error al conectar", "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, "step": { @@ -16,7 +17,7 @@ "password": "Contrase\u00f1a", "port": "Puerto", "site": "ID del sitio", - "username": "Nombre de usuario", + "username": "Usuario", "verify_ssl": "Controlador usando el certificado adecuado" }, "title": "Configurar el controlador UniFi" @@ -52,9 +53,17 @@ "other": "vac\u00edo" } }, + "simple_options": { + "data": { + "block_client": "Acceso controlado a la red de los clientes", + "track_clients": "Rastree clientes de red", + "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)" + }, + "description": "Configurar la integraci\u00f3n de UniFi" + }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" + "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red" }, "description": "Configurar estad\u00edsticas de los sensores", "title": "Opciones UniFi 3/3" diff --git a/homeassistant/components/unifi/translations/fi.json b/homeassistant/components/unifi/translations/fi.json new file mode 100644 index 00000000000..3b9bceb1d0f --- /dev/null +++ b/homeassistant/components/unifi/translations/fi.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "user_privilege": "K\u00e4ytt\u00e4j\u00e4n on oltava j\u00e4rjestelm\u00e4nvalvoja" + }, + "step": { + "user": { + "data": { + "host": "Palvelin", + "password": "Salasana", + "port": "Portti", + "site": "Sivuston ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 007f06b1c8e..8e2a8be41b1 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9", + "no_local_user": "Aucun utilisateur local trouv\u00e9, configurez un compte local sur le contr\u00f4leur et r\u00e9essayez", "user_privilege": "L'utilisateur doit \u00eatre administrateur" }, "error": { @@ -48,6 +49,13 @@ "other": "Vide" } }, + "simple_options": { + "data": { + "track_clients": "Suivi de clients r\u00e9seaux", + "track_devices": "Suivi d'\u00e9quipement r\u00e9seau (Equipements Ubiquiti)" + }, + "description": "Configurer l'int\u00e9gration UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index cde18ca4029..9eddddde302 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", + "no_local_user": "Nessun utente locale trovato, configura un account locale sul controller e riprova", "user_privilege": "L'utente deve essere amministratore" }, "error": { - "faulty_credentials": "Credenziali utente non valide", - "service_unavailable": "Servizio non disponibile", + "faulty_credentials": "Autenticazione non valida", + "service_unavailable": "Impossibile connettersi", "unknown_client_mac": "Nessun client disponibile su quell'indirizzo MAC" }, "step": { @@ -52,6 +53,14 @@ "other": "altri" } }, + "simple_options": { + "data": { + "block_client": "Client controllati per l'accesso alla rete", + "track_clients": "Traccia i client di rete", + "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)" + }, + "description": "Configurare l'integrazione UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete" diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json index 020a5807f30..6878b537209 100644 --- a/homeassistant/components/unifi/translations/ko.json +++ b/homeassistant/components/unifi/translations/ko.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_local_user": "\ub85c\uceec \uc0ac\uc6a9\uc790\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee8\ud2b8\ub864\ub7ec\uc5d0\uc11c \ub85c\uceec \uacc4\uc815\uc744 \uad6c\uc131\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "user_privilege": "\uc0ac\uc6a9\uc790\ub294 \uad00\ub9ac\uc790\uc5ec\uc57c \ud569\ub2c8\ub2e4" }, "error": { - "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc790\uaca9\uc99d\uba85\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "service_unavailable": "\uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "faulty_credentials": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "service_unavailable": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown_client_mac": "\ud574\ub2f9 MAC \uc8fc\uc18c\uc5d0\uc11c \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { @@ -19,7 +20,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "\uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\ub294 \ucee8\ud2b8\ub864\ub7ec" }, - "title": "UniFi \ucee8\ud2b8\ub864\ub7ec \uc124\uc815" + "title": "UniFi \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30" } } }, @@ -37,6 +38,7 @@ "device_tracker": { "data": { "detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)", + "ignore_wired_bug": "UniFi \uc720\uc120 \ubc84\uadf8 \ub85c\uc9c1 \ube44\ud65c\uc131\ud654", "ssid_filter": "\ubb34\uc120 \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \ucd94\uc801\ud558\ub824\uba74 SSID\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1", "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", @@ -45,6 +47,14 @@ "description": "\uae30\uae30 \ucd94\uc801 \uad6c\uc131", "title": "UniFi \uc635\uc158 1/3" }, + "simple_options": { + "data": { + "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", + "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1", + "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)" + }, + "description": "UniFi \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uad6c\uc131" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c" diff --git a/homeassistant/components/unifi/translations/lb.json b/homeassistant/components/unifi/translations/lb.json index 901cde0dab4..d93a1d8d882 100644 --- a/homeassistant/components/unifi/translations/lb.json +++ b/homeassistant/components/unifi/translations/lb.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Kontroller Site ass scho konfigur\u00e9iert", + "no_local_user": "Kee lokale Benotzer fonnt, erstell ee lokale Kont um Kontroller a prob\u00e9ier nach eemol", "user_privilege": "Benotzer muss een Administrator sinn" }, "error": { @@ -52,6 +53,13 @@ "other": "M\u00e9i" } }, + "simple_options": { + "data": { + "track_clients": "Reseau Cliente verfollegen", + "track_devices": "Reseau Apparater verfollege (Ubiquiti Apparater)" + }, + "description": "UniFi Integratioun ariichten" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente" diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index e55ae8fd493..d3ea6b3eaae 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Controller site is al geconfigureerd", + "no_local_user": "Geen lokale gebruiker gevonden, configureer een lokaal account op de controller en probeer het opnieuw", "user_privilege": "Gebruiker moet beheerder zijn" }, "error": { "faulty_credentials": "Foutieve gebruikersgegevens", - "service_unavailable": "Geen service beschikbaar" + "service_unavailable": "Geen service beschikbaar", + "unknown_client_mac": "Geen client beschikbaar op dat MAC-adres" }, "step": { "user": { @@ -26,16 +28,23 @@ "step": { "client_control": { "data": { + "block_client": "Cli\u00ebnten met netwerktoegang", + "new_client": "Voeg een nieuwe client toe voor netwerktoegangsbeheer", "poe_clients": "Sta POE-controle van gebruikers toe" - } + }, + "description": "Configureer clientbesturingen \n\n Maak schakelaars voor serienummers waarvoor u de netwerktoegang wilt beheren.", + "title": "UniFi-opties 2/3" }, "device_tracker": { "data": { "detection_time": "Tijd in seconden vanaf laatst gezien tot beschouwd als weg", + "ignore_wired_bug": "Schakel UniFi bedrade buglogica uit", "track_clients": "Volg netwerkclients", "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" - } + }, + "description": "Apparaattracking configureren", + "title": "UniFi-opties 1/3" }, "init": { "data": { @@ -43,10 +52,17 @@ "other": "Leeg" } }, + "simple_options": { + "data": { + "block_client": "Cli\u00ebnten met netwerktoegang", + "track_clients": "Volg netwerkclients" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" - } + }, + "title": "UniFi-opties 3/3" } } } diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index f1979b7fb97..c244cac1696 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Kontroller nettstedet er allerede konfigurert", + "no_local_user": "Ingen lokale brukere funnet. Konfigurer en lokal konto p\u00e5 kontrolleren og pr\u00f8v igjen", "user_privilege": "Bruker m\u00e5 v\u00e6re administrator" }, "error": { @@ -14,7 +15,7 @@ "data": { "host": "Vert", "password": "Passord", - "port": "", + "port": "Port", "site": "Nettsted-ID", "username": "Brukernavn", "verify_ssl": "Kontroller bruker riktig sertifikat" @@ -46,6 +47,19 @@ "description": "Konfigurere enhetssporing", "title": "UniFi-alternativ 1/3" }, + "init": { + "data": { + "one": "", + "other": "" + } + }, + "simple_options": { + "data": { + "track_clients": "Spor nettverksklienter", + "track_devices": "Spor nettverksenheter (Ubiquiti enheter)" + }, + "description": "Konfigurer UniFi-integrasjon" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter" diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index db35a0b3c6a..27155ff7b5c 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana.", + "no_local_user": "Nie znaleziono lokalnego u\u017cytkownika, skonfiguruj konto lokalne na kontrolerze i spr\u00f3buj ponownie.", "user_privilege": "U\u017cytkownik musi by\u0107 administratorem" }, "error": { @@ -12,11 +13,11 @@ "step": { "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%]", "site": "Identyfikator witryny", - "username": "Nazwa u\u017cytkownika", + "username": "[%key_id:common::config_flow::data::username%]", "verify_ssl": "Kontroler u\u017cywa prawid\u0142owego certyfikatu" }, "title": "Konfiguracja kontrolera UniFi" @@ -37,6 +38,7 @@ "device_tracker": { "data": { "detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.", + "ignore_wired_bug": "Wy\u0142\u0105czanie logiki b\u0142\u0119d\u00f3w dla po\u0142\u0105cze\u0144 przewodowych UniFi", "ssid_filter": "Wybierz SSIDy do \u015bledzenia klient\u00f3w bezprzewodowych", "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)", @@ -53,6 +55,14 @@ "other": "Inne" } }, + "simple_options": { + "data": { + "block_client": "Klienci z kontrol\u0105 dost\u0119pu do sieci", + "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", + "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)" + }, + "description": "Konfigurowanie integracji z UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index ae3e5c6e3f4..f2e404a04fc 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "no_local_user": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0435 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430.", + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "service_unavailable": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, "step": { @@ -53,6 +54,14 @@ "other": "\u0434\u0440\u0443\u0433\u0438\u0435" } }, + "simple_options": { + "data": { + "block_client": "\u041a\u043b\u0438\u0435\u043d\u0442\u044b \u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", + "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 UniFi." + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" diff --git a/homeassistant/components/unifi/translations/sl.json b/homeassistant/components/unifi/translations/sl.json index ec5a1869423..7a5a79e252c 100644 --- a/homeassistant/components/unifi/translations/sl.json +++ b/homeassistant/components/unifi/translations/sl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Nadzornik je \u017ee konfiguriran", + "no_local_user": "Nobenega lokalnega uporabnika ni mogo\u010de najti, konfigurirajte lokalni ra\u010dun na krmilniku in poskusite znova", "user_privilege": "Uporabnik mora biti skrbnik" }, "error": { @@ -37,6 +38,7 @@ "device_tracker": { "data": { "detection_time": "\u010cas v sekundah od zadnjega videnja na omre\u017eju do odsotnosti", + "ignore_wired_bug": "Onemogo\u010di logiko \u017ei\u010dnega hro\u0161\u010da UniFi", "ssid_filter": "Izberite SSID-e za sledenje brez\u017ei\u010dnim odjemalcem", "track_clients": "Sledite odjemalcem omre\u017eja", "track_devices": "Sledite omre\u017enim napravam (naprave Ubiquiti)", @@ -53,6 +55,14 @@ "two": "DVA" } }, + "simple_options": { + "data": { + "block_client": "Nadzorovani mre\u017eni klienti", + "track_clients": "Sledite mre\u017enim klientom", + "track_devices": "Sledite mre\u017enim napravam (Ubiquiti naprave)" + }, + "description": "Nastavite UniFi integracijo" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Senzorji uporabe pasovne \u0161irine za omre\u017ene odjemalce" diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index a41503b5ea2..d3dd18a28e0 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", - "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig" + "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig", + "unknown_client_mac": "Ingen klient tillg\u00e4nglig p\u00e5 den MAC-adressen" }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index 7247293662a..c5d44c13e73 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a", + "no_local_user": "\u627e\u4e0d\u5230\u672c\u5730\u4f7f\u7528\u8005\u3001\u65bc\u63a7\u5236\u5668\u4e0a\u8a2d\u5b9a\u4e00\u7d44\u672c\u5730\u5e33\u865f\u4e26\u518d\u8a66\u4e00\u6b21", "user_privilege": "\u4f7f\u7528\u8005\u5fc5\u9808\u70ba\u7ba1\u7406\u54e1\u8eab\u4efd" }, "error": { - "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548", - "service_unavailable": "\u7121\u670d\u52d9\u53ef\u7528", + "faulty_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", + "service_unavailable": "\u9023\u7dda\u5931\u6557", "unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528\u5ba2\u6236\u7aef" }, "step": { @@ -46,6 +47,14 @@ "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64", "title": "UniFi \u9078\u9805 1/3" }, + "simple_options": { + "data": { + "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", + "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09" + }, + "description": "\u8a2d\u5b9a UniFi \u6574\u5408" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index d8c14105990..9842cdc0777 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -2,35 +2,11 @@ import logging -from aiounifi.api import SOURCE_EVENT -from aiounifi.events import ( - WIRED_CLIENT_BLOCKED, - WIRED_CLIENT_CONNECTED, - WIRED_CLIENT_DISCONNECTED, - WIRED_CLIENT_UNBLOCKED, - WIRELESS_CLIENT_BLOCKED, - WIRELESS_CLIENT_CONNECTED, - WIRELESS_CLIENT_DISCONNECTED, - WIRELESS_CLIENT_ROAM, - WIRELESS_CLIENT_UNBLOCKED, -) - -from homeassistant.components.unifi.unifi_entity_base import UniFiBase -from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -LOGGER = logging.getLogger(__name__) +from .unifi_entity_base import UniFiBase -CLIENT_BLOCKED = (WIRED_CLIENT_BLOCKED, WIRELESS_CLIENT_BLOCKED) -CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED) -WIRED_CLIENT = (WIRED_CLIENT_CONNECTED, WIRED_CLIENT_DISCONNECTED) -WIRELESS_CLIENT_ROAMRADIO = "EVT_WU_RoamRadio" -WIRELESS_CLIENT = ( - WIRELESS_CLIENT_CONNECTED, - WIRELESS_CLIENT_DISCONNECTED, - WIRELESS_CLIENT_ROAM, - WIRELESS_CLIENT_ROAMRADIO, -) +LOGGER = logging.getLogger(__name__) class UniFiClient(UniFiBase): @@ -38,54 +14,14 @@ class UniFiClient(UniFiBase): def __init__(self, client, controller) -> None: """Set up client.""" - self.client = client - super().__init__(controller) + super().__init__(client, controller) - self._is_wired = self.client.mac not in controller.wireless_clients - self.is_blocked = self.client.blocked - self.wired_connection = None - self.wireless_connection = None + self._is_wired = client.mac not in controller.wireless_clients @property - def mac(self): - """Return MAC of client.""" - return self.client.mac - - async def async_added_to_hass(self) -> None: - """Client entity created.""" - await super().async_added_to_hass() - LOGGER.debug("New client %s (%s)", self.entity_id, self.client.mac) - self.client.register_callback(self.async_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect client object when removed.""" - await super().async_will_remove_from_hass() - self.client.remove_callback(self.async_update_callback) - - @callback - def async_update_callback(self) -> None: - """Update the clients state.""" - if self._is_wired and self.client.mac in self.controller.wireless_clients: - self._is_wired = False - - if self.client.last_updated == SOURCE_EVENT: - if self.client.event.event in WIRELESS_CLIENT: - self.wireless_connection = self.client.event.event in ( - WIRELESS_CLIENT_CONNECTED, - WIRELESS_CLIENT_ROAM, - WIRELESS_CLIENT_ROAMRADIO, - ) - - elif self.client.event.event in WIRED_CLIENT: - self.wired_connection = ( - self.client.event.event == WIRED_CLIENT_CONNECTED - ) - - elif self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED: - self.is_blocked = self.client.event.event in CLIENT_BLOCKED - - LOGGER.debug("Updating client %s (%s)", self.entity_id, self.client.mac) - self.async_write_ha_state() + def client(self): + """Wrap item.""" + return self._item @property def is_wired(self): @@ -93,6 +29,9 @@ class UniFiClient(UniFiBase): Allows disabling logic to keep track of clients affected by UniFi wired bug marking wireless devices as wired. This is useful when running a network not only containing UniFi APs. """ + if self._is_wired and self.client.mac in self.controller.wireless_clients: + self._is_wired = False + if self.controller.option_ignore_wired_bug: return self.client.is_wired return self._is_wired diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 94088411411..46a7123e4c9 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -1,10 +1,13 @@ """Base class for UniFi entities.""" +import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_entries_for_device +LOGGER = logging.getLogger(__name__) + class UniFiBase(Entity): """UniFi entity base class.""" @@ -12,44 +15,61 @@ class UniFiBase(Entity): DOMAIN = "" TYPE = "" - def __init__(self, controller) -> None: + def __init__(self, item, controller) -> None: """Set up UniFi entity base. Register mac to controller entities to cover disabled entities. """ + self._item = item self.controller = controller - self.controller.entities[self.DOMAIN][self.TYPE].add(self.mac) - - @property - def mac(self): - """Return MAC of entity.""" - raise NotImplementedError + self.controller.entities[self.DOMAIN][self.TYPE].add(item.mac) async def async_added_to_hass(self) -> None: """Entity created.""" + LOGGER.debug("New %s entity %s (%s)", self.TYPE, self.entity_id, self._item.mac) for signal, method in ( (self.controller.signal_reachable, self.async_update_callback), (self.controller.signal_options_update, self.options_updated), (self.controller.signal_remove, self.remove_item), ): self.async_on_remove(async_dispatcher_connect(self.hass, signal, method)) + self._item.register_callback(self.async_update_callback) async def async_will_remove_from_hass(self) -> None: """Disconnect object when removed.""" - self.controller.entities[self.DOMAIN][self.TYPE].remove(self.mac) + LOGGER.debug( + "Removing %s entity %s (%s)", self.TYPE, self.entity_id, self._item.mac + ) + self._item.remove_callback(self.async_update_callback) + self.controller.entities[self.DOMAIN][self.TYPE].remove(self._item.mac) - async def async_remove(self): - """Clean up when removing entity. + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + LOGGER.debug( + "Updating %s entity %s (%s)", self.TYPE, self.entity_id, self._item.mac + ) + self.async_write_ha_state() + + async def options_updated(self) -> None: + """Config entry options are updated, remove entity if option is disabled.""" + raise NotImplementedError + + async def remove_item(self, mac_addresses: set) -> None: + """Remove entity if MAC is part of set. Remove entity if no entry in entity registry exist. Remove entity registry entry if no entry in device registry exist. Remove device registry entry if there is only one linked entity (this entity). Remove entity registry entry if there are more than one entity linked to the device registry entry. """ + if self._item.mac not in mac_addresses: + return + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() entity_entry = entity_registry.async_get(self.entity_id) if not entity_entry: - await super().async_remove() + await self.async_remove() return device_registry = await self.hass.helpers.device_registry.async_get_registry() @@ -64,21 +84,7 @@ class UniFiBase(Entity): entity_registry.async_remove(self.entity_id) - @callback - def async_update_callback(self): - """Update the entity's state.""" - raise NotImplementedError - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - raise NotImplementedError - - async def remove_item(self, mac_addresses: set) -> None: - """Remove entity if MAC is part of set.""" - if self.mac in mac_addresses: - await self.async_remove() - @property def should_poll(self) -> bool: """No polling needed.""" - return True + return False diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index 6b0b1e2edf1..0281aa351d2 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(UnifiLedLight(light, api) for light in api.getlights()) -class UnifiLedLight(Light): +class UnifiLedLight(LightEntity): """Representation of an unifiled Light.""" def __init__(self, light, api): diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 803793d0683..f1cad7e8abf 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, @@ -116,7 +116,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([player]) -class UniversalMediaPlayer(MediaPlayerDevice): +class UniversalMediaPlayer(MediaPlayerEntity): """Representation of an universal media player.""" def __init__(self, hass, name, children, commands, attributes, state_template=None): diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py new file mode 100644 index 00000000000..f2765ff317d --- /dev/null +++ b/homeassistant/components/upb/__init__.py @@ -0,0 +1,150 @@ +"""Support the UPB PIM.""" +import asyncio + +import upb_lib + +from homeassistant.const import CONF_FILE_PATH, CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_ADDRESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COMMAND, + ATTR_RATE, + DOMAIN, + EVENT_UPB_SCENE_CHANGED, +) + +UPB_PLATFORMS = ["light", "scene"] + + +async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: + """Set up the UPB platform.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up a new config_entry for UPB PIM.""" + + url = config_entry.data[CONF_HOST] + file = config_entry.data[CONF_FILE_PATH] + + upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file}) + upb.connect() + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} + + for component in UPB_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + def _element_changed(element, changeset): + change = changeset.get("last_change") + if change is None: + return + if change.get("command") is None: + return + + hass.bus.async_fire( + EVENT_UPB_SCENE_CHANGED, + { + ATTR_COMMAND: change["command"], + ATTR_ADDRESS: element.addr.index, + ATTR_BRIGHTNESS_PCT: change.get("level", -1), + ATTR_RATE: change.get("rate", -1), + }, + ) + + for link in upb.links: + element = upb.links[link] + element.add_callback(_element_changed) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload the config_entry.""" + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in UPB_PLATFORMS + ] + ) + ) + + if unload_ok: + upb = hass.data[DOMAIN][config_entry.entry_id]["upb"] + upb.disconnect() + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +class UpbEntity(Entity): + """Base class for all UPB entities.""" + + def __init__(self, element, unique_id, upb): + """Initialize the base of all UPB devices.""" + self._upb = upb + self._element = element + element_type = "link" if element.addr.is_link else "device" + self._unique_id = f"{unique_id}_{element_type}_{element.addr}" + + @property + def name(self): + """Name of the element.""" + return self._element.name + + @property + def unique_id(self): + """Return unique id of the element.""" + return self._unique_id + + @property + def should_poll(self) -> bool: + """Don't poll this device.""" + return False + + @property + def device_state_attributes(self): + """Return the default attributes of the element.""" + return self._element.as_dict() + + @property + def available(self): + """Is the entity available to be updated.""" + return self._upb.is_connected() + + def _element_changed(self, element, changeset): + pass + + @callback + def _element_callback(self, element, changeset): + """Handle callback from an UPB element that has changed.""" + self._element_changed(element, changeset) + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register callback for UPB changes and update entity state.""" + self._element.add_callback(self._element_callback) + self._element_callback(self._element, {}) + + +class UpbAttachedEntity(UpbEntity): + """Base class for UPB attached entities.""" + + @property + def device_info(self): + """Device info for the entity.""" + return { + "name": self._element.name, + "identifiers": {(DOMAIN, self._element.index)}, + "sw_version": self._element.version, + "manufacturer": self._element.manufacturer, + "model": self._element.product, + } diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py new file mode 100644 index 00000000000..a84b71c71e4 --- /dev/null +++ b/homeassistant/components/upb/config_flow.py @@ -0,0 +1,140 @@ +"""Config flow for UPB PIM integration.""" +import asyncio +import logging +from urllib.parse import urlparse + +import async_timeout +import upb_lib +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_ADDRESS, CONF_FILE_PATH, CONF_HOST, CONF_PROTOCOL + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) +PROTOCOL_MAP = {"TCP": "tcp://", "Serial port": "serial://"} +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PROTOCOL, default="Serial port"): vol.In( + ["TCP", "Serial port"] + ), + vol.Required(CONF_ADDRESS): str, + vol.Required(CONF_FILE_PATH, default=""): str, + } +) +VALIDATE_TIMEOUT = 15 + + +async def _validate_input(data): + """Validate the user input allows us to connect.""" + + def _connected_callback(): + connected_event.set() + + connected_event = asyncio.Event() + file_path = data.get(CONF_FILE_PATH) + url = _make_url_from_data(data) + + upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file_path}) + if not upb.config_ok: + _LOGGER.error("Missing or invalid UPB file: %s", file_path) + raise InvalidUpbFile + + upb.connect(_connected_callback) + + try: + with async_timeout.timeout(VALIDATE_TIMEOUT): + await connected_event.wait() + except asyncio.TimeoutError: + pass + + upb.disconnect() + + if not connected_event.is_set(): + _LOGGER.error( + "Timed out after %d seconds trying to connect with UPB PIM at %s", + VALIDATE_TIMEOUT, + url, + ) + raise CannotConnect + + # Return info that you want to store in the config entry. + return (upb.network_id, {"title": "UPB", CONF_HOST: url, CONF_FILE_PATH: file_path}) + + +def _make_url_from_data(data): + host = data.get(CONF_HOST) + if host: + return host + + protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]] + address = data[CONF_ADDRESS] + return f"{protocol}{address}" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for UPB PIM.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the UPB config flow.""" + self.importing = False + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + if self._url_already_configured(_make_url_from_data(user_input)): + return self.async_abort(reason="address_already_configured") + network_id, info = await _validate_input(user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidUpbFile: + errors["base"] = "invalid_upb_file" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(network_id) + self._abort_if_unique_id_configured() + + if self.importing: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_create_entry( + title=info["title"], + data={ + CONF_HOST: info[CONF_HOST], + CONF_FILE_PATH: user_input[CONF_FILE_PATH], + }, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + self.importing = True + return await self.async_step_user(user_input) + + def _url_already_configured(self, url): + """See if we already have a UPB PIM matching user input configured.""" + existing_hosts = { + urlparse(entry.data[CONF_HOST]).hostname + for entry in self._async_current_entries() + } + return urlparse(url).hostname in existing_hosts + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidUpbFile(exceptions.HomeAssistantError): + """Error to indicate there is invalid or missing UPB config file.""" diff --git a/homeassistant/components/upb/const.py b/homeassistant/components/upb/const.py new file mode 100644 index 00000000000..75d754087e4 --- /dev/null +++ b/homeassistant/components/upb/const.py @@ -0,0 +1,37 @@ +"""Support the UPB PIM.""" + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +DOMAIN = "upb" + +ATTR_ADDRESS = "address" +ATTR_BLINK_RATE = "blink_rate" +ATTR_BRIGHTNESS = "brightness" +ATTR_BRIGHTNESS_PCT = "brightness_pct" +ATTR_COMMAND = "command" +ATTR_RATE = "rate" +CONF_NETWORK = "network" +EVENT_UPB_SCENE_CHANGED = "upb.scene_changed" + +VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) +VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) +VALID_RATE = vol.All(vol.Coerce(float), vol.Clamp(min=-1, max=3600)) + +UPB_BRIGHTNESS_RATE_SCHEMA = vol.All( + cv.has_at_least_one_key(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT), + cv.make_entity_service_schema( + { + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, + vol.Optional(ATTR_RATE, default=-1): VALID_RATE, + } + ), +) + +UPB_BLINK_RATE_SCHEMA = { + vol.Required(ATTR_BLINK_RATE, default=0.5): vol.All( + vol.Coerce(float), vol.Range(min=0, max=4.25) + ) +} diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py new file mode 100644 index 00000000000..78a983254bb --- /dev/null +++ b/homeassistant/components/upb/light.py @@ -0,0 +1,104 @@ +"""Platform for UPB light integration.""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_FLASH, + ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + LightEntity, +) +from homeassistant.helpers import entity_platform + +from . import UpbAttachedEntity +from .const import DOMAIN, UPB_BLINK_RATE_SCHEMA, UPB_BRIGHTNESS_RATE_SCHEMA + +SERVICE_LIGHT_FADE_START = "light_fade_start" +SERVICE_LIGHT_FADE_STOP = "light_fade_stop" +SERVICE_LIGHT_BLINK = "light_blink" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the UPB light based on a config entry.""" + + upb = hass.data[DOMAIN][config_entry.entry_id]["upb"] + unique_id = config_entry.entry_id + async_add_entities( + UpbLight(upb.devices[dev], unique_id, upb) for dev in upb.devices + ) + + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_LIGHT_FADE_START, UPB_BRIGHTNESS_RATE_SCHEMA, "async_light_fade_start" + ) + platform.async_register_entity_service( + SERVICE_LIGHT_FADE_STOP, {}, "async_light_fade_stop" + ) + platform.async_register_entity_service( + SERVICE_LIGHT_BLINK, UPB_BLINK_RATE_SCHEMA, "async_light_blink" + ) + + +class UpbLight(UpbAttachedEntity, LightEntity): + """Representation of an UPB Light.""" + + def __init__(self, element, unique_id, upb): + """Initialize an UpbLight.""" + super().__init__(element, unique_id, upb) + self._brightness = self._element.status + + @property + def supported_features(self): + """Flag supported features.""" + if self._element.dimmable: + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH + return SUPPORT_FLASH + + @property + def brightness(self): + """Get the brightness.""" + return self._brightness + + @property + def is_on(self) -> bool: + """Get the current brightness.""" + return self._brightness != 0 + + async def async_turn_on(self, **kwargs): + """Turn on the light.""" + flash = kwargs.get(ATTR_FLASH) + if flash: + await self.async_light_blink(0.5 if flash == "short" else 1.5) + else: + rate = kwargs.get(ATTR_TRANSITION, -1) + brightness = round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55) + self._element.turn_on(brightness, rate) + + async def async_turn_off(self, **kwargs): + """Turn off the device.""" + rate = kwargs.get(ATTR_TRANSITION, -1) + self._element.turn_off(rate) + + async def async_light_fade_start(self, rate, brightness=None, brightness_pct=None): + """Start dimming of device.""" + if brightness is not None: + brightness_pct = round(brightness / 2.55) + self._element.fade_start(brightness_pct, rate) + + async def async_light_fade_stop(self): + """Stop dimming of device.""" + self._element.fade_stop() + + async def async_light_blink(self, blink_rate): + """Request device to blink.""" + blink_rate = int(blink_rate * 60) # Convert seconds to 60 hz pulses + self._element.blink(blink_rate) + + async def async_update(self): + """Request the device to update its status.""" + self._element.update_status() + + def _element_changed(self, element, changeset): + status = self._element.status + self._brightness = round(status * 2.55) if status else 0 diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json new file mode 100644 index 00000000000..1f170030df6 --- /dev/null +++ b/homeassistant/components/upb/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "upb", + "name": "Universal Powerline Bus (UPB)", + "documentation": "https://www.home-assistant.io/integrations/upb", + "requirements": ["upb_lib==0.4.11"], + "codeowners": ["@gwww"], + "config_flow": true +} diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py new file mode 100644 index 00000000000..81a2f23c3c6 --- /dev/null +++ b/homeassistant/components/upb/scene.py @@ -0,0 +1,72 @@ +"""Platform for UPB link integration.""" +from typing import Any + +from homeassistant.components.scene import Scene +from homeassistant.helpers import entity_platform + +from . import UpbEntity +from .const import DOMAIN, UPB_BLINK_RATE_SCHEMA, UPB_BRIGHTNESS_RATE_SCHEMA + +SERVICE_LINK_DEACTIVATE = "link_deactivate" +SERVICE_LINK_FADE_STOP = "link_fade_stop" +SERVICE_LINK_GOTO = "link_goto" +SERVICE_LINK_FADE_START = "link_fade_start" +SERVICE_LINK_BLINK = "link_blink" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the UPB link based on a config entry.""" + upb = hass.data[DOMAIN][config_entry.entry_id]["upb"] + unique_id = config_entry.entry_id + async_add_entities(UpbLink(upb.links[link], unique_id, upb) for link in upb.links) + + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_LINK_DEACTIVATE, {}, "async_link_deactivate" + ) + platform.async_register_entity_service( + SERVICE_LINK_FADE_STOP, {}, "async_link_fade_stop" + ) + platform.async_register_entity_service( + SERVICE_LINK_GOTO, UPB_BRIGHTNESS_RATE_SCHEMA, "async_link_goto" + ) + platform.async_register_entity_service( + SERVICE_LINK_FADE_START, UPB_BRIGHTNESS_RATE_SCHEMA, "async_link_fade_start" + ) + platform.async_register_entity_service( + SERVICE_LINK_BLINK, UPB_BLINK_RATE_SCHEMA, "async_link_blink" + ) + + +class UpbLink(UpbEntity, Scene): + """Representation of an UPB Link.""" + + async def async_activate(self, **kwargs: Any) -> None: + """Activate the task.""" + self._element.activate() + + async def async_link_deactivate(self): + """Activate the task.""" + self._element.deactivate() + + async def async_link_goto(self, rate, brightness=None, brightness_pct=None): + """Activate the task.""" + if brightness is not None: + brightness_pct = round(brightness / 2.55) + self._element.goto(brightness_pct, rate) + + async def async_link_fade_start(self, rate, brightness=None, brightness_pct=None): + """Start dimming a link.""" + if brightness is not None: + brightness_pct = round(brightness / 2.55) + self._element.fade_start(brightness_pct, rate) + + async def async_link_fade_stop(self): + """Stop dimming a link.""" + self._element.fade_stop() + + async def async_link_blink(self, blink_rate): + """Blink a link.""" + blink_rate = int(blink_rate * 60) + self._element.blink(blink_rate) diff --git a/homeassistant/components/upb/services.yaml b/homeassistant/components/upb/services.yaml new file mode 100644 index 00000000000..661c95ba991 --- /dev/null +++ b/homeassistant/components/upb/services.yaml @@ -0,0 +1,88 @@ +light_fade_start: + description: Start fading a light either up or down from current brightness. + fields: + entity_id: + description: Name(s) of lights to start fading + example: "light.kitchen" + brightness: + description: Number between 0 and 255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness. + example: 142 + brightness_pct: + description: Number between 0 and 100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness. + example: 42 + rate: + description: Rate for light to transition to new brightness + example: 3 + +light_fade_stop: + description: Stop a light fade. + fields: + entity_id: + description: Name(s) of lights to stop fadding + example: "light.kitchen, light.family_room" + +light_blink: + description: Blink a light + fields: + entity_id: + description: Name(s) of lights to start fading + example: "light.kitchen" + rate: + description: Number of seconds between 0 and 4.25 that the link flashes on. + example: 4.2 + +link_deactivate: + description: Deactivate a UPB scene. + fields: + entity_id: + description: Name(s) of scenes to deactivate + example: "scene.hygge" + +link_goto: + description: Set scene to brightness. + fields: + entity_id: + description: Name(s) of scenes to deactivate + example: "scene.hygge" + brightness: + description: Number between 0 and 255 indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. + example: 120 + brightness_pct: + description: Number between 0 and 100 indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. + example: 42 + rate: + description: Rate in seconds for scene to transition to new brightness + example: 3.42 + +link_fade_start: + description: Start fading a link either up or down from current brightness. + fields: + entity_id: + description: Name(s) of links to start fading + example: "scene.party" + brightness: + description: Number between 0 and 255 indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. + example: 142 + brightness_pct: + description: Number between 0 and 100 indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. + example: 42 + rate: + description: Rate in seconds for scene to transition to new brightness + example: 3.42 + +link_fade_stop: + description: Stop a link fade. + fields: + entity_id: + description: Name(s) of links to stop fadding + example: "scene.dining, scene.no_tv" + +link_blink: + description: Blink a link. + fields: + entity_id: + description: Name(s) of links to start fading + example: "scene.hygge" + blink_rate: + description: Number of seconds between 0 and 4.25 that the link flashes on. + example: 1.5 diff --git a/homeassistant/components/upb/strings.json b/homeassistant/components/upb/strings.json new file mode 100644 index 00000000000..fb4f82d555e --- /dev/null +++ b/homeassistant/components/upb/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to UPB PIM", + "description": "Connect a Universal Powerline Bus Powerline Interface Module (UPB PIM). The address string must be in the form 'address[:port]' for 'tcp'. The port is optional and defaults to 2101. Example: '192.168.1.42'. For the serial protocol, the address must be in the form 'tty[:baud]'. The baud is optional and defaults to 4800. Example: '/dev/ttyS1'.", + "data": { + "protocol": "Protocol", + "address": "Address (see description above)", + "file_path": "Path and name of the UPStart UPB export file." + } + } + }, + "error": { + "cannot_connect": "Failed to connect to UPB PIM, please try again.", + "invalid_upb_file": "Missing or invalid UPB UPStart export file, check the name and path of the file.", + "unknown": "Unexpected error." + }, + "abort": { + "address_already_configured": "An UPB PIM with this address is already configured." + } + } +} diff --git a/homeassistant/components/upb/translations/ca.json b/homeassistant/components/upb/translations/ca.json new file mode 100644 index 00000000000..b54e2816572 --- /dev/null +++ b/homeassistant/components/upb/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ja hi ha un UPB PIM configurat amb aquesta adre\u00e7a." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar a UPB PIM, torna-ho a provar.", + "invalid_upb_file": "El fitxer d\u2019exportaci\u00f3 UPB UPStart no hi \u00e9s o \u00e9s erroni, comprova el nom i la ruta del fitxer.", + "unknown": "Error inesperat." + }, + "step": { + "user": { + "data": { + "address": "Adre\u00e7a (veure descripci\u00f3 de dalt)", + "file_path": "Ruta i nom del fitxer d'exportaci\u00f3 UPStart UPB.", + "protocol": "Protocol" + }, + "title": "Connexi\u00f3 amb UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json new file mode 100644 index 00000000000..12ecaebe688 --- /dev/null +++ b/homeassistant/components/upb/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ein UPB PIM mit dieser Adresse ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Fehler beim Herstellen einer Verbindung zu UPB PIM. Versuchen Sie es erneut.", + "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfen Sie den Namen und den Pfad der Datei.", + "unknown": "Unerwarteter Fehler." + }, + "step": { + "user": { + "data": { + "address": "Adresse (siehe Beschreibung oben)", + "file_path": "Pfad und Name der UPStart UPB-Exportdatei.", + "protocol": "Protokoll" + }, + "title": "Stellen Sie eine Verbindung zu UPB PIM her" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/en.json b/homeassistant/components/upb/translations/en.json new file mode 100644 index 00000000000..61aa19e64f0 --- /dev/null +++ b/homeassistant/components/upb/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "An UPB PIM with this address is already configured." + }, + "error": { + "cannot_connect": "Failed to connect to UPB PIM, please try again.", + "invalid_upb_file": "Missing or invalid UPB UPStart export file, check the name and path of the file.", + "unknown": "Unexpected error." + }, + "step": { + "user": { + "data": { + "address": "Address (see description above)", + "file_path": "Path and name of the UPStart UPB export file.", + "protocol": "Protocol" + }, + "description": "Connect a Universal Powerline Bus Powerline Interface Module (UPB PIM). The address string must be in the form 'address[:port]' for 'tcp'. The port is optional and defaults to 2101. Example: '192.168.1.42'. For the serial protocol, the address must be in the form 'tty[:baud]'. The baud is optional and defaults to 4800. Example: '/dev/ttyS1'.", + "title": "Connect to UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/es.json b/homeassistant/components/upb/translations/es.json new file mode 100644 index 00000000000..15e251ac48a --- /dev/null +++ b/homeassistant/components/upb/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un PIM UPB con esta direcci\u00f3n ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se conect\u00f3 con UPB PIM, por favor, int\u00e9ntelo de nuevo.", + "invalid_upb_file": "Archivo de exportaci\u00f3n UPB UPStart faltante o no v\u00e1lido, verifique el nombre y la ruta del archivo.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "address": "Direcci\u00f3n (v\u00e9ase la descripci\u00f3n anterior)", + "file_path": "Ruta y nombre del archivo de exportaci\u00f3n UPStart UPB.", + "protocol": "Protocolo" + }, + "description": "Conecte un M\u00f3dulo de Interfaz Universal Powerline Bus Powerline (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n [: puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty [: baudios]'. El baud es opcional y el valor predeterminado es 4800. Ejemplo: '/ dev / ttyS1'.", + "title": "Conectar con UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/fi.json b/homeassistant/components/upb/translations/fi.json new file mode 100644 index 00000000000..0f05925f38e --- /dev/null +++ b/homeassistant/components/upb/translations/fi.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Odottamaton virhe." + }, + "step": { + "user": { + "data": { + "protocol": "Protokolla" + }, + "title": "Yhdist\u00e4 UPB PIM:\u00e4\u00e4n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/fr.json b/homeassistant/components/upb/translations/fr.json new file mode 100644 index 00000000000..7f914fd23e8 --- /dev/null +++ b/homeassistant/components/upb/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "Erreur inattendue." + }, + "step": { + "user": { + "data": { + "address": "Adresse (voir description ci-dessus)", + "file_path": "Chemin et nom du fichier d'exportation UPStart UPB.", + "protocol": "Protocole" + }, + "title": "Se connecter \u00e0 UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/he.json b/homeassistant/components/upb/translations/he.json new file mode 100644 index 00000000000..ece7d57a907 --- /dev/null +++ b/homeassistant/components/upb/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "address": "\u05db\u05ea\u05d5\u05d1\u05ea (\u05e8\u05d0\u05d4 \u05ea\u05d9\u05d0\u05d5\u05e8 \u05dc\u05de\u05e2\u05dc\u05d4)", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/it.json b/homeassistant/components/upb/translations/it.json new file mode 100644 index 00000000000..cd858b86989 --- /dev/null +++ b/homeassistant/components/upb/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un UPB PIM con questo indirizzo \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi a UPB PIM, riprovare.", + "invalid_upb_file": "File di esportazione UPStart UPB mancante o non valido, controllare il nome e il percorso del file.", + "unknown": "Errore imprevisto." + }, + "step": { + "user": { + "data": { + "address": "Indirizzo (vedi descrizione sopra)", + "file_path": "Percorso e nome del file di esportazione UPStart UPB.", + "protocol": "Protocollo" + }, + "description": "Collegare un Modulo Interfaccia Powerline del Bus Universale Powerline (UPB PIM). La stringa dell'indirizzo deve essere nel formato 'address[:port]' per 'tcp'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101. Esempio: '192.168.1.42'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Baud \u00e8 opzionale e il valore predefinito \u00e8 4800. Esempio: '/dev/ttyS1'.", + "title": "Collegamento a UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/ko.json b/homeassistant/components/upb/translations/ko.json new file mode 100644 index 00000000000..b1890ead5ed --- /dev/null +++ b/homeassistant/components/upb/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "\uc774 \uc8fc\uc18c\ub85c UPB PIM \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "UPB PIM \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_upb_file": "UPB UPStart \ub0b4\ubcf4\ub0b4\uae30 \ud30c\uc77c\uc774 \uc5c6\uac70\ub098 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud30c\uc77c \uc774\ub984\uacfc \uacbd\ub85c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "address": "\uc8fc\uc18c (\uc0c1\ub2e8\uc758 \uc124\uba85\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694)", + "file_path": "UPStart UPB \ub0b4\ubcf4\ub0b4\uae30 \ud30c\uc77c\uc758 \uacbd\ub85c \ubc0f \uc774\ub984", + "protocol": "\ud504\ub85c\ud1a0\ucf5c" + }, + "description": "\ubc94\uc6a9 \ud30c\uc6cc\ub77c\uc778 \ubc84\uc2a4 \ud30c\uc6cc\ub77c\uc778 \uc778\ud130\ud398\uc774\uc2a4 \ubaa8\ub4c8 (UPB PIM) \uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694. \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tcp' \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 2101 \uc785\ub2c8\ub2e4. \uc608: '192.168.1.42'. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tty[:baud]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ubcf4(baud)\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 4800 \uc785\ub2c8\ub2e4. \uc608: '/dev/ttyS1'.", + "title": "UPB PIM \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/lb.json b/homeassistant/components/upb/translations/lb.json new file mode 100644 index 00000000000..26a76f8ab2e --- /dev/null +++ b/homeassistant/components/upb/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Feeler beim verbannen mat UPB PIM, prob\u00e9iert w.e.g. nach emol.", + "invalid_upb_file": "UPB UPStart export fichier feelt oder ong\u00eblteg, iwwerpr\u00e9if den numm a pad vum fichier.", + "unknown": "Onerwaarte Feeler." + }, + "step": { + "user": { + "data": { + "address": "Adress (Kuck Beschr\u00e9iwung uewen)", + "protocol": "Protokoll" + }, + "description": "Verbann een Universal Bus Powerline Interface Module (UPB PIM). D'Adresse muss an der der From 'adress[:port]' fir 'tcp' sinn. De Port ass optionell an als standard op 2101 gesat. Beispill: '192.168.1.42'. Fir de serielle Protokoll muss d'Adress an der form 'tty[:baud]' sinn. Baudrate ass optionell an standardm\u00e9isseg o p 4800. Beispill: '/dev/ttyS1'.", + "title": "Mat UPB PIM verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/no.json b/homeassistant/components/upb/translations/no.json new file mode 100644 index 00000000000..2dfe583fda0 --- /dev/null +++ b/homeassistant/components/upb/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "En UPB PIM med denne adressen er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kunne ikke koble til UPB PIM, pr\u00f8v igjen.", + "invalid_upb_file": "Manglende eller ugyldig UPB UPStart eksportfil, sjekk navnet og banen til filen.", + "unknown": "Uventet feil." + }, + "step": { + "user": { + "data": { + "address": "Adresse (se beskrivelse over)", + "file_path": "Sti og navn p\u00e5 UPStart UPB-eksportfilen.", + "protocol": "protokoll" + }, + "description": "Koble til en universal Powerline Bus Powerline Interface Module (UPB PIM). Adressestrengen m\u00e5 v\u00e6re i skjemaet 'adresse[:port]' for 'tcp'. Porten er valgfri og bruker som standard til 2101. Eksempel: '192.168.1.42'. For serieprotokollen m\u00e5 adressen v\u00e6re i skjemaet 'tty[:baud]'. Baud er valgfritt og standard til 4800. Eksempel: '/dev/ttyS1'.", + "title": "Koble til UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/pl.json b/homeassistant/components/upb/translations/pl.json new file mode 100644 index 00000000000..a236c6fded2 --- /dev/null +++ b/homeassistant/components/upb/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "[%key_id:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/ru.json b/homeassistant/components/upb/translations/ru.json new file mode 100644 index 00000000000..563e190d397 --- /dev/null +++ b/homeassistant/components/upb/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_upb_file": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0444\u0430\u0439\u043b \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0430 UPB UPStart, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438\u043c\u044f \u0438 \u043f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 (\u0441\u043c. \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0432\u044b\u0448\u0435)", + "file_path": "\u041f\u0443\u0442\u044c \u0438 \u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0430 UPStart UPB.", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'tcp' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '192.168.1.42'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 2101. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'serial' \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 4800.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/sv.json b/homeassistant/components/upb/translations/sv.json new file mode 100644 index 00000000000..0120bd8fe08 --- /dev/null +++ b/homeassistant/components/upb/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "address_already_configured": "En UPB PIM med denna adress \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till UPB PIM, f\u00f6rs\u00f6k igen.", + "invalid_upb_file": "Saknar eller ogiltig UPB UPStart-exportfil, kontrollera filens namn och s\u00f6kv\u00e4g.", + "unknown": "Ov\u00e4ntat fel." + }, + "step": { + "user": { + "data": { + "address": "Adress (se beskrivning ovan)", + "protocol": "Protokoll" + }, + "title": "Anslut till UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/zh-Hant.json b/homeassistant/components/upb/translations/zh-Hant.json new file mode 100644 index 00000000000..527262121e1 --- /dev/null +++ b/homeassistant/components/upb/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u4f7f\u7528\u6b64\u4f4d\u5740\u7684\u4e00\u7d44 UPB PIM \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "UPB PIM \u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_upb_file": "UPB UPStart \u532f\u51fa\u6a94\u6848\u7121\u6548\u6216\u907a\u5931\uff0c\u8acb\u78ba\u8a8d\u6a94\u6848\u7684\u8def\u5f91\u8207\u540d\u7a31\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "address": "\u4f4d\u5740\uff08\u8acb\u53c3\u95b1\u4e0a\u65b9\u8aaa\u660e\uff09", + "file_path": "UPStart UPB \u532f\u51fa\u6a94\u6848\u4e4b\u8def\u5f91\u8207\u540d\u7a31\u3002", + "protocol": "\u901a\u8a0a\u5354\u5b9a" + }, + "description": "\u9023\u7dda\u81f3 Universal Powerline Bus Powerline Interface Module (UPB PIM)\u3002'tcp' \u5354\u5b9a\u4e4b\u4f4d\u5740\u5b57\u4e32\u5fc5\u9808\u4ee5 'address[:port]' \u683c\u5f0f\u8f38\u5165\u3002\u901a\u8a0a\u57e0\u70ba\u9078\u9805\u8f38\u5165\u3001\u9810\u8a2d\u70ba 2101\u3002\u4f8b\u5982\uff1a'192.168.1.42'\u3002\u5e8f\u5217\u5354\u5b9a\u4f4d\u5740\u5fc5\u9808\u4ee5 'tty[:baud]' \u683c\u5f0f\u8f38\u5165\u3002\u9b91\u7387\u70ba\u9078\u9805\u8f38\u5165\u3001\u9810\u8a2d\u70ba 4800\u3002\u4f8b\u5982\uff1a'/dev/ttyS1'.", + "title": "\u9023\u7dda\u81f3 UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/binary_sensor.py b/homeassistant/components/upcloud/binary_sensor.py index 12324599958..2b29aa0e24f 100644 --- a/homeassistant/components/upcloud/binary_sensor.py +++ b/homeassistant/components/upcloud/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity import homeassistant.helpers.config_validation as cv from . import CONF_SERVERS, DATA_UPCLOUD, UpCloudServerEntity @@ -26,5 +26,5 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorDevice): +class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorEntity): """Representation of an UpCloud server sensor.""" diff --git a/homeassistant/components/upcloud/switch.py b/homeassistant/components/upcloud/switch.py index 5cb1d86671e..1d0984cdddb 100644 --- a/homeassistant/components/upcloud/switch.py +++ b/homeassistant/components/upcloud/switch.py @@ -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 STATE_OFF import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class UpCloudSwitch(UpCloudServerEntity, SwitchDevice): +class UpCloudSwitch(UpCloudServerEntity, SwitchEntity): """Representation of an UpCloud server switch.""" def turn_on(self, **kwargs): diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 5771a4f0cfe..869e3c55271 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,9 +1,7 @@ """Support to check for available updates.""" from datetime import timedelta from distutils.version import StrictVersion -import json import logging -import uuid import async_timeout from distro import linux_distribution # pylint: disable=import-error @@ -25,7 +23,6 @@ CONF_COMPONENT_REPORTING = "include_used_components" DOMAIN = "updater" UPDATER_URL = "https://updater.home-assistant.io/" -UPDATER_UUID_FILE = ".uuid" CONFIG_SCHEMA = vol.Schema( { @@ -52,26 +49,6 @@ class Updater: self.newest_version = newest_version -def _create_uuid(hass, filename=UPDATER_UUID_FILE): - """Create UUID and save it in a file.""" - with open(hass.config.path(filename), "w") as fptr: - _uuid = uuid.uuid4().hex - fptr.write(json.dumps({"uuid": _uuid})) - return _uuid - - -def _load_uuid(hass, filename=UPDATER_UUID_FILE): - """Load UUID from a file or return None.""" - try: - with open(hass.config.path(filename)) as fptr: - jsonf = json.loads(fptr.read()) - return uuid.UUID(jsonf["uuid"], version=4).hex - except (ValueError, AttributeError): - return None - except FileNotFoundError: - return _create_uuid(hass, filename) - - async def async_setup(hass, config): """Set up the updater component.""" if "dev" in current_version: @@ -80,7 +57,7 @@ async def async_setup(hass, config): conf = config.get(DOMAIN, {}) if conf.get(CONF_REPORTING): - huuid = await hass.async_add_job(_load_uuid, hass) + huuid = await hass.helpers.instance_id.async_get() else: huuid = None diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 7abab616d5c..5088856ce6d 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,6 +1,6 @@ """Support for Home Assistant Updater binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN @@ -13,7 +13,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([UpdaterBinary(hass.data[UPDATER_DOMAIN])]) -class UpdaterBinary(BinarySensorDevice): +class UpdaterBinary(BinarySensorEntity): """Representation of an updater binary sensor.""" def __init__(self, coordinator): diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 4d599be88b1..65049db8c4f 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,24 +1,24 @@ """Open ports in your router for Home Assistant and provide statistics.""" from ipaddress import ip_address from operator import itemgetter -from typing import Mapping import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from .const import ( - CONF_ENABLE_PORT_MAPPING, - CONF_ENABLE_SENSORS, - CONF_HASS, CONF_LOCAL_IP, - CONF_PORTS, + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, DOMAIN, LOGGER as _LOGGER, ) @@ -28,112 +28,64 @@ NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, - vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, - vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - vol.Optional(CONF_PORTS, default={}): vol.Schema( - {vol.Any(CONF_HASS, cv.port): vol.Any(CONF_HASS, cv.port)} - ), - } - ) - }, + {DOMAIN: vol.Schema({vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string)})}, extra=vol.ALLOW_EXTRA, ) -def _substitute_hass_ports(ports: Mapping, hass_port: int = None) -> Mapping: - """ - Substitute 'hass' for the hass_port. - - This triggers a warning when hass_port is None. - """ - ports = ports.copy() - - # substitute 'hass' for hass_port, both keys and values - if CONF_HASS in ports: - if hass_port is None: - _LOGGER.warning( - "Could not determine Home Assistant http port, " - "not setting up port mapping from %s to %s. " - "Enable the http-component.", - CONF_HASS, - ports[CONF_HASS], - ) - else: - ports[hass_port] = ports[CONF_HASS] - del ports[CONF_HASS] - - for port in ports: - if ports[port] == CONF_HASS: - if hass_port is None: - _LOGGER.warning( - "Could not determine Home Assistant http port, " - "not setting up port mapping from %s to %s. " - "Enable the http-component.", - port, - ports[port], - ) - del ports[port] - else: - ports[port] = hass_port - - return ports - - async def async_discover_and_construct( hass: HomeAssistantType, udn: str = None, st: str = None ) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name discovery_infos = await Device.async_discover(hass) + _LOGGER.debug("Discovered devices: %s", discovery_infos) if not discovery_infos: _LOGGER.info("No UPnP/IGD devices discovered") return None if udn: - # get the discovery info with specified UDN - _LOGGER.debug("Discovery_infos: %s", discovery_infos) - filtered = [di for di in discovery_infos if di["udn"] == udn] + # Get the discovery info with specified UDN/ST. + filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn] if st: - _LOGGER.debug("Filtering on ST: %s", st) - filtered = [di for di in discovery_infos if di["st"] == st] + filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st] if not filtered: _LOGGER.warning( - 'Wanted UPnP/IGD device with UDN "%s" not found, ' "aborting", udn + 'Wanted UPnP/IGD device with UDN "%s" not found, aborting', udn ) return None - # ensure we're always taking the latest - filtered = sorted(filtered, key=itemgetter("st"), reverse=True) + + # Ensure we're always taking the latest, if we filtered only on UDN. + filtered = sorted(filtered, key=itemgetter(DISCOVERY_ST), reverse=True) discovery_info = filtered[0] else: - # get the first/any + # Get the first/any. discovery_info = discovery_infos[0] if len(discovery_infos) > 1: device_name = discovery_info.get( - "usn", discovery_info.get("ssdp_description", "") + DISCOVERY_USN, discovery_info.get(DISCOVERY_LOCATION, "") ) _LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name) - ssdp_description = discovery_info["ssdp_description"] - return await Device.async_create_device(hass, ssdp_description) + location = discovery_info[DISCOVERY_LOCATION] + return await Device.async_create_device(hass, location) async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up UPnP component.""" + _LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] conf = config.get(DOMAIN, conf_default) local_ip = await hass.async_add_executor_job(get_local_ip) hass.data[DOMAIN] = { "config": conf, "devices": {}, + "coordinators": {}, "local_ip": conf.get(CONF_LOCAL_IP, local_ip), - "ports": conf.get(CONF_PORTS), } - if conf is not None: + # Only start if set up via configuration.yaml. + if DOMAIN in config: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} @@ -145,25 +97,26 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - domain_data = hass.data[DOMAIN] - conf = domain_data["config"] + _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data) # discover and construct - udn = config_entry.data.get("udn") - st = config_entry.data.get("st") # pylint: disable=invalid-name + udn = config_entry.data.get(CONFIG_ENTRY_UDN) + st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name device = await async_discover_and_construct(hass, udn, st) if not device: _LOGGER.info("Unable to create UPnP/IGD, aborting") raise ConfigEntryNotReady - # 'register'/save UDN + ST + # Save device hass.data[DOMAIN]["devices"][device.udn] = device - hass.config_entries.async_update_entry( - entry=config_entry, - data={**config_entry.data, "udn": device.udn, "st": device.device_type}, - ) - # create device registry entry + # Ensure entry has proper unique_id. + if config_entry.unique_id != device.unique_id: + hass.config_entries.async_update_entry( + entry=config_entry, unique_id=device.unique_id, + ) + + # Create device registry entry. device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -174,35 +127,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) model=device.model_name, ) - # set up sensors - if conf.get(CONF_ENABLE_SENSORS): - _LOGGER.debug("Enabling sensors") - - # register sensor setup handlers - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) - - # set up port mapping - if conf.get(CONF_ENABLE_PORT_MAPPING): - _LOGGER.debug("Enabling port mapping") - local_ip = domain_data[CONF_LOCAL_IP] - ports = conf.get(CONF_PORTS, {}) - - hass_port = None - if hasattr(hass, "http"): - hass_port = hass.http.server_port - - ports = _substitute_hass_ports(ports, hass_port=hass_port) - await device.async_add_port_mappings(ports, local_ip) - - # set up port mapping deletion on stop-hook - async def delete_port_mapping(event): - """Delete port mapping on quit.""" - _LOGGER.debug("Deleting port mappings") - await device.async_delete_port_mappings() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping) + # Create sensors. + _LOGGER.debug("Enabling sensors") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) return True @@ -211,13 +140,9 @@ async def async_unload_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: """Unload a UPnP/IGD device from a config entry.""" - udn = config_entry.data["udn"] - device = hass.data[DOMAIN]["devices"][udn] + udn = config_entry.data.get(CONFIG_ENTRY_UDN) + del hass.data[DOMAIN]["devices"][udn] + del hass.data[DOMAIN]["coordinators"][udn] - # remove port mapping - _LOGGER.debug("Deleting port mappings") - await device.async_delete_port_mappings() - - # remove sensors _LOGGER.debug("Deleting sensors") return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 1601595b6a9..a85e47c5919 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,10 +1,245 @@ """Config flow for UPNP.""" -from homeassistant import config_entries -from homeassistant.helpers import config_entry_flow +from datetime import timedelta +from typing import Mapping, Optional -from .const import DOMAIN +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import ssdp +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import callback + +from .const import ( # pylint: disable=unused-import + CONFIG_ENTRY_SCAN_INTERVAL, + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DEFAULT_SCAN_INTERVAL, + DISCOVERY_LOCATION, + DISCOVERY_NAME, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, + DOMAIN, + LOGGER as _LOGGER, +) from .device import Device -config_entry_flow.register_discovery_flow( - DOMAIN, "UPnP/IGD", Device.async_discover, config_entries.CONN_CLASS_LOCAL_POLL -) + +class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UPnP/IGD config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + # Paths: + # - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() + # - user(None): scan --> user({...}) --> create_entry() + # - import(None) --> create_entry() + + def __init__(self): + """Initialize the UPnP/IGD config flow.""" + self._discoveries: Mapping = None + + async def async_step_user(self, user_input: Optional[Mapping] = None): + """Handle a flow start.""" + _LOGGER.debug("async_step_user: user_input: %s", user_input) + # This uses DISCOVERY_USN as the identifier for the device. + + if user_input is not None: + # Ensure wanted device was discovered. + matching_discoveries = [ + discovery + for discovery in self._discoveries + if discovery[DISCOVERY_USN] == user_input["usn"] + ] + if not matching_discoveries: + return self.async_abort(reason="no_devices_discovered") + + discovery = matching_discoveries[0] + await self.async_set_unique_id( + discovery[DISCOVERY_USN], raise_on_progress=False + ) + return await self._async_create_entry_from_discovery(discovery) + + # Discover devices. + discoveries = await Device.async_discover(self.hass) + + # Store discoveries which have not been configured, add name for each discovery. + current_usns = {entry.unique_id for entry in self._async_current_entries()} + self._discoveries = [ + { + **discovery, + DISCOVERY_NAME: await self._async_get_name_for_discovery(discovery), + } + for discovery in discoveries + if discovery[DISCOVERY_USN] not in current_usns + ] + + # Ensure anything to add. + if not self._discoveries: + return self.async_abort(reason="no_devices_found") + + data_schema = vol.Schema( + { + vol.Required("usn"): vol.In( + { + discovery[DISCOVERY_USN]: discovery[DISCOVERY_NAME] + for discovery in self._discoveries + } + ), + } + ) + return self.async_show_form(step_id="user", data_schema=data_schema,) + + async def async_step_import(self, import_info: Optional[Mapping]): + """Import a new UPnP/IGD device as a config entry. + + This flow is triggered by `async_setup`. If no device has been + configured before, find any device and create a config_entry for it. + Otherwise, do nothing. + """ + _LOGGER.debug("async_step_import: import_info: %s", import_info) + + if import_info is None: + # Landed here via configuration.yaml entry. + # Any device already added, then abort. + if self._async_current_entries(): + _LOGGER.debug("aborting, already configured") + return self.async_abort(reason="already_configured") + + # Test if import_info isn't already configured. + if import_info is not None and any( + import_info["udn"] == entry.data[CONFIG_ENTRY_UDN] + and import_info["st"] == entry.data[CONFIG_ENTRY_ST] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + # Discover devices. + self._discoveries = await Device.async_discover(self.hass) + + # Ensure anything to add. If not, silently abort. + if not self._discoveries: + _LOGGER.info("No UPnP devices discovered, aborting.") + return self.async_abort(reason="no_devices_found") + + discovery = self._discoveries[0] + return await self._async_create_entry_from_discovery(discovery) + + async def async_step_ssdp(self, discovery_info: Mapping): + """Handle a discovered UPnP/IGD device. + + This flow is triggered by the SSDP component. It will check if the + host is already configured and delegate to the import step if not. + """ + _LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) + + # Ensure complete discovery. + if ( + ssdp.ATTR_UPNP_UDN not in discovery_info + or ssdp.ATTR_SSDP_ST not in discovery_info + ): + _LOGGER.debug("Incomplete discovery, ignoring") + return self.async_abort(reason="incomplete_discovery") + + # Ensure not already configuring/configured. + udn = discovery_info[ssdp.ATTR_UPNP_UDN] + st = discovery_info[ssdp.ATTR_SSDP_ST] # pylint: disable=invalid-name + usn = f"{udn}::{st}" + await self.async_set_unique_id(usn) + self._abort_if_unique_id_configured() + + # Store discovery. + name = discovery_info.get("friendlyName", "") + discovery = { + DISCOVERY_UDN: udn, + DISCOVERY_ST: st, + DISCOVERY_NAME: name, + } + self._discoveries = [discovery] + + # Ensure user recognizable. + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + "name": name, + } + + return await self.async_step_ssdp_confirm() + + async def async_step_ssdp_confirm(self, user_input: Optional[Mapping] = None): + """Confirm integration via SSDP.""" + _LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) + if user_input is None: + return self.async_show_form(step_id="ssdp_confirm") + + discovery = self._discoveries[0] + return await self._async_create_entry_from_discovery(discovery) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return UpnpOptionsFlowHandler(config_entry) + + async def _async_create_entry_from_discovery( + self, discovery: Mapping, + ): + """Create an entry from discovery.""" + _LOGGER.debug( + "_async_create_entry_from_data: discovery: %s", discovery, + ) + # Get name from device, if not found already. + if DISCOVERY_NAME not in discovery and DISCOVERY_LOCATION in discovery: + discovery[DISCOVERY_NAME] = await self._async_get_name_for_discovery( + discovery + ) + + title = discovery.get(DISCOVERY_NAME, "") + data = { + CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN], + CONFIG_ENTRY_ST: discovery[DISCOVERY_ST], + } + return self.async_create_entry(title=title, data=data) + + async def _async_get_name_for_discovery(self, discovery: Mapping): + """Get the name of the device from a discovery.""" + _LOGGER.debug("_async_get_name_for_discovery: discovery: %s", discovery) + device = await Device.async_create_device( + self.hass, discovery[DISCOVERY_LOCATION] + ) + return device.name + + +class UpnpOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a UPnP options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + udn = self.config_entry.data.get(CONFIG_ENTRY_UDN) + coordinator = self.hass.data[DOMAIN]["coordinators"][udn] + update_interval_sec = user_input.get( + CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + update_interval = timedelta(seconds=update_interval_sec) + _LOGGER.debug("Updating coordinator, update_interval: %s", update_interval) + coordinator.update_interval = update_interval + return self.async_create_entry(title="", data=user_input) + + scan_interval = self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional(CONF_SCAN_INTERVAL, default=scan_interval,): vol.All( + vol.Coerce(int), vol.Range(min=30) + ), + } + ), + ) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 80b5b718bbb..eb0844e2cb0 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -4,11 +4,7 @@ import logging from homeassistant.const import TIME_SECONDS -CONF_ENABLE_PORT_MAPPING = "port_mapping" -CONF_ENABLE_SENSORS = "sensors" -CONF_HASS = "hass" CONF_LOCAL_IP = "local_ip" -CONF_PORTS = "ports" DOMAIN = "upnp" LOGGER = logging.getLogger(__package__) BYTES_RECEIVED = "bytes_received" @@ -20,3 +16,12 @@ DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) +DISCOVERY_NAME = "name" +DISCOVERY_LOCATION = "location" +DISCOVERY_ST = "st" +DISCOVERY_UDN = "udn" +DISCOVERY_USN = "usn" +CONFIG_ENTRY_UDN = "udn" +CONFIG_ENTRY_ST = "st" +CONFIG_ENTRY_SCAN_INTERVAL = "scan_interval" +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).seconds diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 73ae06d9945..05113b8f9f6 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,10 +1,9 @@ """Home Assistant representation of an UPnP/IGD.""" import asyncio from ipaddress import IPv4Address -from typing import Mapping +from typing import List, Mapping -import aiohttp -from async_upnp_client import UpnpError, UpnpFactory +from async_upnp_client import UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.profiles.igd import IgdDevice @@ -16,6 +15,10 @@ from .const import ( BYTES_RECEIVED, BYTES_SENT, CONF_LOCAL_IP, + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, DOMAIN, LOGGER as _LOGGER, PACKETS_RECEIVED, @@ -33,7 +36,7 @@ class Device: self._mapped_ports = [] @classmethod - async def async_discover(cls, hass: HomeAssistantType): + async def async_discover(cls, hass: HomeAssistantType) -> List[Mapping]: """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = None @@ -47,9 +50,11 @@ class Device: # add extra info and store devices devices = [] for discovery_info in discovery_infos: - discovery_info["udn"] = discovery_info["_udn"] - discovery_info["ssdp_description"] = discovery_info["location"] - discovery_info["source"] = "async_upnp_client" + discovery_info[DISCOVERY_UDN] = discovery_info["_udn"] + discovery_info[DISCOVERY_ST] = discovery_info["st"] + discovery_info[DISCOVERY_LOCATION] = discovery_info["location"] + usn = f"{discovery_info[DISCOVERY_UDN]}::{discovery_info[DISCOVERY_ST]}" + discovery_info[DISCOVERY_USN] = usn _LOGGER.debug("Discovered device: %s", discovery_info) devices.append(discovery_info) @@ -57,7 +62,7 @@ class Device: return devices @classmethod - async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str): + async def async_create_device(cls, hass: HomeAssistantType, ssdp_location: str): """Create UPnP/IGD device.""" # build async_upnp_client requester session = async_get_clientsession(hass) @@ -65,7 +70,7 @@ class Device: # create async_upnp_client device factory = UpnpFactory(requester, disable_state_variable_validation=True) - upnp_device = await factory.async_create_device(ssdp_description) + upnp_device = await factory.async_create_device(ssdp_location) igd_device = IgdDevice(upnp_device, None) @@ -96,74 +101,15 @@ class Device: """Get the device type.""" return self._igd_device.device_type + @property + def unique_id(self) -> str: + """Get the unique id.""" + return f"{self.udn}::{self.device_type}" + def __str__(self) -> str: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}" - async def async_add_port_mappings( - self, ports: Mapping[int, int], local_ip: str - ) -> None: - """Add port mappings.""" - if local_ip == "127.0.0.1": - _LOGGER.error("Could not create port mapping, our IP is 127.0.0.1") - - # determine local ip, ensure sane IP - local_ip = IPv4Address(local_ip) - - # create port mappings - for external_port, internal_port in ports.items(): - await self._async_add_port_mapping(external_port, local_ip, internal_port) - self._mapped_ports.append(external_port) - - async def _async_add_port_mapping( - self, external_port: int, local_ip: str, internal_port: int - ) -> None: - """Add a port mapping.""" - # create port mapping - _LOGGER.info( - "Creating port mapping %s:%s:%s (TCP)", - external_port, - local_ip, - internal_port, - ) - try: - await self._igd_device.async_add_port_mapping( - remote_host=None, - external_port=external_port, - protocol="TCP", - internal_port=internal_port, - internal_client=local_ip, - enabled=True, - description="Home Assistant", - lease_duration=None, - ) - - self._mapped_ports.append(external_port) - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - _LOGGER.error( - "Could not add port mapping: %s:%s:%s", - external_port, - local_ip, - internal_port, - ) - - async def async_delete_port_mappings(self) -> None: - """Remove port mappings.""" - for port in self._mapped_ports: - await self._async_delete_port_mapping(port) - - async def _async_delete_port_mapping(self, external_port: int) -> None: - """Remove a port mapping.""" - _LOGGER.info("Deleting port mapping %s (TCP)", external_port) - try: - await self._igd_device.async_delete_port_mapping( - remote_host=None, external_port=external_port, protocol="TCP" - ) - - self._mapped_ports.remove(external_port) - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - _LOGGER.error("Could not delete port mapping") - async def async_get_traffic_data(self) -> Mapping[str, any]: """ Get all traffic data in one go. diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2f6e5de5884..e3b30cec9a4 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -5,5 +5,13 @@ "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.14.13"], "dependencies": [], - "codeowners": ["@StevenLooman"] + "codeowners": ["@StevenLooman"], + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + }, + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" + } + ] } diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 5c356b53c8a..29bdf7429ab 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -12,15 +12,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( BYTES_RECEIVED, BYTES_SENT, + CONFIG_ENTRY_SCAN_INTERVAL, + CONFIG_ENTRY_UDN, DATA_PACKETS, DATA_RATE_PACKETS_PER_SECOND, + DEFAULT_SCAN_INTERVAL, DOMAIN, KIBIBYTE, LOGGER as _LOGGER, PACKETS_RECEIVED, PACKETS_SENT, TIMESTAMP, - UPDATE_INTERVAL, ) from .device import Device @@ -78,23 +80,29 @@ async def async_setup_entry( ) -> None: """Set up the UPnP/IGD sensors.""" data = config_entry.data - if "udn" in data: - udn = data["udn"] + if CONFIG_ENTRY_UDN in data: + udn = data[CONFIG_ENTRY_UDN] else: # any device will do udn = list(hass.data[DOMAIN]["devices"].keys())[0] device: Device = hass.data[DOMAIN]["devices"][udn] + update_interval_sec = config_entry.options.get( + CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + update_interval = timedelta(seconds=update_interval_sec) + _LOGGER.debug("update_interval: %s", update_interval) _LOGGER.debug("Adding sensors") coordinator = DataUpdateCoordinator( hass, _LOGGER, name=device.name, update_method=device.async_get_traffic_data, - update_interval=timedelta(seconds=UPDATE_INTERVAL.seconds), + update_interval=update_interval, ) await coordinator.async_refresh() + hass.data[DOMAIN]["coordinators"][udn] = coordinator sensors = [ RawUpnpSensor(coordinator, device, SENSOR_TYPES[BYTES_RECEIVED]), @@ -117,11 +125,14 @@ class UpnpSensor(Entity): coordinator: DataUpdateCoordinator, device: Device, sensor_type: Mapping[str, str], + update_multiplier: int = 2, ) -> None: """Initialize the base sensor.""" self._coordinator = coordinator self._device = device self._sensor_type = sensor_type + self._update_counter_max = update_multiplier + self._update_counter = 0 @property def should_poll(self) -> bool: diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 5ad90b2c0cb..99e58698f2e 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,25 +1,24 @@ { "config": { + "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Do you want to set up UPnP/IGD?" + "init": { + }, + "ssdp_confirm": { + "description": "Do you want to set up this UPnP/IGD device?" }, "user": { - "title": "Configuration options", "data": { - "enable_port_mapping": "Enable port mapping for Home Assistant", - "enable_sensors": "Add traffic sensors", - "igd": "UPnP/IGD" + "usn": "Device", + "scan_interval": "Update interval (seconds, minimal 30)" } } }, "abort": { "already_configured": "UPnP/IGD is already configured", - "incomplete_device": "Ignoring incomplete UPnP device", "no_devices_discovered": "No UPnP/IGDs discovered", "no_devices_found": "No UPnP/IGD devices found on the network.", - "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", - "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." + "incomplete_discovery": "Incomplete discovery" } } } diff --git a/homeassistant/components/upnp/translations/bg.json b/homeassistant/components/upnp/translations/bg.json index d99458ca4e7..7e85d64daa1 100644 --- a/homeassistant/components/upnp/translations/bg.json +++ b/homeassistant/components/upnp/translations/bg.json @@ -17,9 +17,6 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0437\u0430 Home Assistant", diff --git a/homeassistant/components/upnp/translations/ca.json b/homeassistant/components/upnp/translations/ca.json index f58069ea907..e0824e5f9bb 100644 --- a/homeassistant/components/upnp/translations/ca.json +++ b/homeassistant/components/upnp/translations/ca.json @@ -12,19 +12,22 @@ "one": "un", "other": "altre" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Vols configurar UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Vols configurar aquest dispositiu UPnP/IGD?" }, "user": { "data": { "enable_port_mapping": "Activa l'assignaci\u00f3 de ports per a Home Assistant", "enable_sensors": "Afegeix sensors de tr\u00e0nsit", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)", + "usn": "Dispositiu" }, "title": "Opcions de configuraci\u00f3 d'UPnP/IGD" } diff --git a/homeassistant/components/upnp/translations/cs.json b/homeassistant/components/upnp/translations/cs.json index 745b136bd5d..9e3878bedd4 100644 --- a/homeassistant/components/upnp/translations/cs.json +++ b/homeassistant/components/upnp/translations/cs.json @@ -13,9 +13,6 @@ "description": "Chcete nastavit UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Povolit mapov\u00e1n\u00ed port\u016f pro Home Assistant", diff --git a/homeassistant/components/upnp/translations/da.json b/homeassistant/components/upnp/translations/da.json index 7f75272b240..e45e84fffaf 100644 --- a/homeassistant/components/upnp/translations/da.json +++ b/homeassistant/components/upnp/translations/da.json @@ -17,9 +17,6 @@ "description": "Er du sikker p\u00e5 at du vil konfigurere UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Aktiv\u00e9r porttilknytning til Home Assistent", diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index 14f8f472221..bfe95b10a39 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -12,19 +12,21 @@ "one": "Ein", "other": "andere" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "M\u00f6chtest du UPnP/IGD einrichten?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "M\u00f6chten Sie dieses UPnP/IGD-Ger\u00e4t einrichten?" }, "user": { "data": { "enable_port_mapping": "Aktiviere Port-Mapping f\u00fcr Home Assistant", "enable_sensors": "Verkehrssensoren hinzuf\u00fcgen", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "usn": "Ger\u00e4t" }, "title": "Konfigurations-Optionen" } diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index 6da89c0e3d6..d5436028cba 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -8,19 +8,22 @@ "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Do you want to set up UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Do you want to set up this UPnP/IGD device?" }, "user": { "data": { "enable_port_mapping": "Enable port mapping for Home Assistant", "enable_sensors": "Add traffic sensors", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "scan_interval": "Update interval (seconds, minimal 30)", + "usn": "Device" }, "title": "Configuration options" } diff --git a/homeassistant/components/upnp/translations/es-419.json b/homeassistant/components/upnp/translations/es-419.json index 00d43221727..e516d978af0 100644 --- a/homeassistant/components/upnp/translations/es-419.json +++ b/homeassistant/components/upnp/translations/es-419.json @@ -13,9 +13,6 @@ "description": "\u00bfDesea configurar UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index cf07aac4bde..6ca30b11339 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -12,21 +12,24 @@ "one": "UNO", "other": "OTRO" }, + "flow_title": "UPnP / IGD: {name}", "step": { "confirm": { "description": "\u00bfDesea configurar UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP / IGD" + "ssdp_confirm": { + "description": "\u00bfQuieres configurar este dispositivo UPnP/IGD?" }, "user": { "data": { "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico", - "igd": "UPnP / IGD" + "igd": "UPnP / IGD", + "scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)", + "usn": "Dispositivo" }, - "title": "Opciones de configuraci\u00f3n para UPnP/IGD" + "title": "Opciones de configuraci\u00f3n" } } } diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json index bfbd2137298..bfffa5783ff 100644 --- a/homeassistant/components/upnp/translations/et.json +++ b/homeassistant/components/upnp/translations/et.json @@ -1,9 +1,6 @@ { "config": { "step": { - "init": { - "title": "" - }, "user": { "data": { "igd": "" diff --git a/homeassistant/components/upnp/translations/fi.json b/homeassistant/components/upnp/translations/fi.json new file mode 100644 index 00000000000..0ad39900b72 --- /dev/null +++ b/homeassistant/components/upnp/translations/fi.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 UPnP/IGD:n?", + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_sensors": "Lis\u00e4\u00e4 liikenneanturit", + "igd": "UPnP/IGD", + "scan_interval": "P\u00e4ivitysv\u00e4li (sekuntia, v\u00e4hint\u00e4\u00e4n 30)", + "usn": "Laite" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index 8b46143beda..6baf9087b8e 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -12,19 +12,21 @@ "one": "Vide", "other": "Vide" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Voulez-vous configurer UPnP / IGD?", "title": "UPnP / IGD" }, - "init": { - "title": "UPnP / IGD" + "ssdp_confirm": { + "description": "Voulez-vous configurer ce p\u00e9riph\u00e9rique UPnP/IGD?" }, "user": { "data": { "enable_port_mapping": "Activer le mappage de port pour Home Assistant", "enable_sensors": "Ajouter des capteurs de trafic", - "igd": "UPnP / IGD" + "igd": "UPnP / IGD", + "usn": "Appareil" }, "title": "Options de configuration pour UPnP / IGD" } diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json new file mode 100644 index 00000000000..4b922ccd2ba --- /dev/null +++ b/homeassistant/components/upnp/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "usn": "\u05de\u05db\u05e9\u05d9\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index e44afa53a65..a8dc4ce854b 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -16,9 +16,6 @@ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a UPnP/IGD-t?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Enged\u00e9lyezd a port mappinget a Home Assistant sz\u00e1m\u00e1ra", diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index 06c40aa95a7..df13cc79d6f 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -12,21 +12,24 @@ "one": "Vuoto", "other": "Vuoto" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Vuoi configurare UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Vuoi configurare questo dispositivo UPnP/IGD?" }, "user": { "data": { "enable_port_mapping": "Abilita il port mapping per Home Assistant", "enable_sensors": "Aggiungi sensori di traffico", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)", + "usn": "Dispositivo" }, - "title": "Opzioni di configurazione per UPnP/IGD" + "title": "Opzioni di configurazione" } } } diff --git a/homeassistant/components/upnp/translations/ko.json b/homeassistant/components/upnp/translations/ko.json index d1581b026cc..b3a0d822a2b 100644 --- a/homeassistant/components/upnp/translations/ko.json +++ b/homeassistant/components/upnp/translations/ko.json @@ -8,21 +8,24 @@ "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "UPnP/IGD \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "\uc774 UPnP/IGD \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { "enable_port_mapping": "Home Assistant \ud3ec\ud2b8 \ub9e4\ud551 \ud65c\uc131\ud654", "enable_sensors": "\ud2b8\ub798\ud53d \uc13c\uc11c \ucd94\uac00", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc18c\uac12 30)", + "usn": "\uae30\uae30" }, - "title": "UPnP/IGD \uc758 \uad6c\uc131 \uc635\uc158" + "title": "\uc635\uc158 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/upnp/translations/lb.json b/homeassistant/components/upnp/translations/lb.json index 9fb009f0935..e30bae93d06 100644 --- a/homeassistant/components/upnp/translations/lb.json +++ b/homeassistant/components/upnp/translations/lb.json @@ -12,19 +12,22 @@ "one": "Een", "other": "Aaner" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Soll UPnP/IGD konfigur\u00e9iert ginn?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Soll d\u00ebsen UPnP/IGD Apparat konfigur\u00e9iert ginn?" }, "user": { "data": { "enable_port_mapping": "Port Mapping fir Home Assistant aktiv\u00e9ieren", "enable_sensors": "Trafic Sensoren dob\u00e4isetzen", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "scan_interval": "Update Intervall (Sekonnen, minimum 30)", + "usn": "Apparat" }, "title": "Konfiguratiouns Optiounen" } diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index f86eb49e0c4..c36e6252867 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -12,19 +12,21 @@ "one": "Een", "other": "Ander" }, + "flow_title": "[%%]: {naam}", "step": { "confirm": { "description": "Wilt u UPnP/IGD instellen?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Wilt u [%%] instellen?" }, "user": { "data": { "enable_port_mapping": "Poorttoewijzing voor Home Assistant inschakelen", "enable_sensors": "Voeg verkeerssensoren toe", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "usn": "Apparaat" }, "title": "Configuratiemogelijkheden voor de UPnP/IGD" } diff --git a/homeassistant/components/upnp/translations/nn.json b/homeassistant/components/upnp/translations/nn.json index b947ce87ff5..f521406b087 100644 --- a/homeassistant/components/upnp/translations/nn.json +++ b/homeassistant/components/upnp/translations/nn.json @@ -11,9 +11,6 @@ "confirm": { "title": "UPnP/IGD" }, - "init": { - "title": "UPnP / IGD" - }, "user": { "data": { "igd": "UPnP/IGD" diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index 3004ab40ee7..99eb7925b5c 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -16,21 +16,24 @@ "two": "to", "zero": "ingen" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 konfigurere UPnP / IGD?", - "title": "UPnP / IGD" + "description": "\u00d8nsker du \u00e5 sette opp UPnP / IGD?", + "title": "" }, - "init": { - "title": "UPnP / IGD" + "ssdp_confirm": { + "description": "\u00d8nsker du \u00e5 sette opp denne UPnP/IGD-enheten?" }, "user": { "data": { "enable_port_mapping": "Aktiver port mapping for Home Assistant", "enable_sensors": "Legg til trafikk sensorer", - "igd": "UPnP / IGD" + "igd": "UPnP / IGD", + "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)", + "usn": "Enhet" }, - "title": "Konfigurasjonsalternativer for UPnP / IGD" + "title": "Konfigurasjonsalternativer" } } } diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json index a4370672a96..08e20e75679 100644 --- a/homeassistant/components/upnp/translations/pl.json +++ b/homeassistant/components/upnp/translations/pl.json @@ -8,25 +8,21 @@ "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w", "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja UPnP/IGD." }, - "error": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 to urz\u0105dzenie UPnP/IGD?" }, "user": { "data": { "enable_port_mapping": "W\u0142\u0105cz mapowanie port\u00f3w dla Home Assistant'a", "enable_sensors": "Dodaj sensor ruchu sieciowego", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "usn": "Urz\u0105dzenie" }, "title": "Opcje konfiguracji dla UPnP/IGD" } diff --git a/homeassistant/components/upnp/translations/pt-BR.json b/homeassistant/components/upnp/translations/pt-BR.json index 25804bab983..d472fa18834 100644 --- a/homeassistant/components/upnp/translations/pt-BR.json +++ b/homeassistant/components/upnp/translations/pt-BR.json @@ -13,9 +13,6 @@ "description": "Deseja configurar o UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", diff --git a/homeassistant/components/upnp/translations/pt.json b/homeassistant/components/upnp/translations/pt.json index 1dbbc34ab0b..ea501c2c263 100644 --- a/homeassistant/components/upnp/translations/pt.json +++ b/homeassistant/components/upnp/translations/pt.json @@ -17,9 +17,6 @@ "description": "Deseja configurar o UPnP / IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", diff --git a/homeassistant/components/upnp/translations/ro.json b/homeassistant/components/upnp/translations/ro.json index 7c8401569f1..33342807995 100644 --- a/homeassistant/components/upnp/translations/ro.json +++ b/homeassistant/components/upnp/translations/ro.json @@ -10,9 +10,6 @@ "other": "" }, "step": { - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Activa\u021bi maparea porturilor pentru Home Assistant", diff --git a/homeassistant/components/upnp/translations/ru.json b/homeassistant/components/upnp/translations/ru.json index 2e8a3ac3b45..eaf75d85b09 100644 --- a/homeassistant/components/upnp/translations/ru.json +++ b/homeassistant/components/upnp/translations/ru.json @@ -14,19 +14,22 @@ "one": "\u043e\u0434\u0438\u043d", "other": "\u0434\u0440\u0443\u0433\u0438\u0435" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c UPnP / IGD?", "title": "UPnP / IGD" }, - "init": { - "title": "UPnP / IGD" + "ssdp_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e UPnP / IGD?" }, "user": { "data": { "enable_port_mapping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 \u0434\u043b\u044f Home Assistant", "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430", - "igd": "UPnP / IGD" + "igd": "UPnP / IGD", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)", + "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "title": "UPnP / IGD" } diff --git a/homeassistant/components/upnp/translations/sl.json b/homeassistant/components/upnp/translations/sl.json index 2ff0acc208c..1813461eadd 100644 --- a/homeassistant/components/upnp/translations/sl.json +++ b/homeassistant/components/upnp/translations/sl.json @@ -14,19 +14,21 @@ "other": "ve\u010d", "two": "dve" }, + "flow_title": "UPnP/IGD: {name}", "step": { "confirm": { "description": "Ali \u017eelite nastaviti UPnp/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "Ali \u017eelite nastaviti to UPnP/IGD napravo?" }, "user": { "data": { "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistant-a", "enable_sensors": "Dodaj prometne senzorje", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "usn": "Naprava" }, "title": "Mo\u017enosti konfiguracije za UPnP/IGD" } diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index 67584ed2f34..f53c9294ea9 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -17,14 +17,12 @@ "description": "Vill du konfigurera UPnP/IGD?", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "Aktivera portmappning f\u00f6r Home Assistant", "enable_sensors": "L\u00e4gg till trafiksensorer", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "usn": "Enheten" }, "title": "Konfigurationsalternativ f\u00f6r UPnP/IGD" } diff --git a/homeassistant/components/upnp/translations/zh-Hans.json b/homeassistant/components/upnp/translations/zh-Hans.json index 2c367a3e88f..80216d7a18e 100644 --- a/homeassistant/components/upnp/translations/zh-Hans.json +++ b/homeassistant/components/upnp/translations/zh-Hans.json @@ -13,14 +13,12 @@ "description": "\u60a8\u60f3\u8981\u914d\u7f6e UPnP/IGD \u5417\uff1f", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" - }, "user": { "data": { "enable_port_mapping": "\u4e3a Home Assistant \u542f\u7528\u7aef\u53e3\u6620\u5c04", "enable_sensors": "\u6dfb\u52a0\u6d41\u91cf\u4f20\u611f\u5668", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "usn": "\u8bbe\u5907" }, "title": "UPnP/IGD \u7684\u914d\u7f6e\u9009\u9879" } diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index 5464ce9e74b..c45157ff77d 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -8,19 +8,22 @@ "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 UPnP/IGD \u5373\u53ef\u3002" }, + "flow_title": "UPnP/IGD\uff1a{name}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD\uff1f", "title": "UPnP/IGD" }, - "init": { - "title": "UPnP/IGD" + "ssdp_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u8a2d\u5099\uff1f" }, "user": { "data": { "enable_port_mapping": "\u958b\u555f Home Assistant \u901a\u8a0a\u57e0\u8f49\u767c", "enable_sensors": "\u65b0\u589e\u6d41\u91cf\u611f\u61c9\u5668", - "igd": "UPnP/IGD" + "igd": "UPnP/IGD", + "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", + "usn": "\u8a2d\u5099" }, "title": "\u8a2d\u5b9a\u9078\u9805" } diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 401da496d2f..231dce9a402 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -4,7 +4,7 @@ import logging from pyuptimerobot import UptimeRobot import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class UptimeRobotBinarySensor(BinarySensorDevice): +class UptimeRobotBinarySensor(BinarySensorEntity): """Representation of a Uptime Robot binary sensor.""" def __init__(self, api_key, up_robot, monitor_id, name, target): diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 5878023bf2e..3e0363f4f35 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -1,5 +1,6 @@ """Support for Ubiquiti's UVC cameras.""" import logging +import re import requests from uvcclient import camera as uvc_camera, nvr @@ -213,7 +214,16 @@ class UnifiVideoCamera(Camera): """Return the source of the stream.""" for channel in self._caminfo["channels"]: if channel["isRtspEnabled"]: - return channel["rtspUris"][0] + uri = next( + ( + uri + for i, uri in enumerate(channel["rtspUris"]) + # pylint: disable=protected-access + if re.search(self._nvr._host, uri) + # pylint: enable=protected-access + ) + ) + return uri return None diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index f66a1b5f226..111fa64f988 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -228,7 +228,7 @@ class _BaseVacuum(Entity): ) -class VacuumDevice(_BaseVacuum, ToggleEntity): +class VacuumEntity(_BaseVacuum, ToggleEntity): """Representation of a vacuum cleaner robot.""" @property @@ -249,7 +249,7 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): @property def capability_attributes(self): """Return capability attributes.""" - if self.fan_speed is not None: + if self.supported_features & SUPPORT_FAN_SPEED: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} @property @@ -309,7 +309,18 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): """Not supported.""" -class StateVacuumDevice(_BaseVacuum): +class VacuumDevice(VacuumEntity): + """Representation of a vacuum (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "VacuumDevice is deprecated, modify %s to extend VacuumEntity", cls.__name__ + ) + + +class StateVacuumEntity(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" @property @@ -329,7 +340,7 @@ class StateVacuumDevice(_BaseVacuum): @property def capability_attributes(self): """Return capability attributes.""" - if self.fan_speed is not None: + if self.supported_features & SUPPORT_FAN_SPEED: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} @property @@ -377,3 +388,15 @@ class StateVacuumDevice(_BaseVacuum): async def async_toggle(self, **kwargs): """Not supported.""" + + +class StateVacuumDevice(StateVacuumEntity): + """Representation of a vacuum (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "StateVacuumDevice is deprecated, modify %s to extend StateVacuumEntity", + cls.__name__, + ) diff --git a/homeassistant/components/vacuum/translations/es-419.json b/homeassistant/components/vacuum/translations/es-419.json index 39ed128de9d..59126d08a42 100644 --- a/homeassistant/components/vacuum/translations/es-419.json +++ b/homeassistant/components/vacuum/translations/es-419.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "clean": "Deje que {entity_name} limpie", + "dock": "Dejar que {entity_name} regrese al dock" + }, + "condition_type": { + "is_cleaning": "{entity_name} est\u00e1 limpiando", + "is_docked": "{entity_name} est\u00e1 acoplado" + }, + "trigger_type": { + "cleaning": "{entity_name} comenz\u00f3 a limpiar", + "docked": "{entity_name} acoplado" + } + }, "state": { "_": { "cleaning": "Limpiando", diff --git a/homeassistant/components/vacuum/translations/ko.json b/homeassistant/components/vacuum/translations/ko.json index e82b47bc5be..0e59d5157eb 100644 --- a/homeassistant/components/vacuum/translations/ko.json +++ b/homeassistant/components/vacuum/translations/ko.json @@ -21,7 +21,7 @@ "idle": "\ub300\uae30\uc911", "off": "\uaebc\uc9d0", "on": "\ucf1c\uc9d0", - "paused": "\uc77c\uc2dc\uc911\uc9c0\ub428", + "paused": "\uc77c\uc2dc\uc911\uc9c0", "returning": "\ucda9\uc804 \ubcf5\uadc0 \uc911" } }, diff --git a/homeassistant/components/vacuum/translations/no.json b/homeassistant/components/vacuum/translations/no.json index dbf94d1243b..8e62bc64e81 100644 --- a/homeassistant/components/vacuum/translations/no.json +++ b/homeassistant/components/vacuum/translations/no.json @@ -1,12 +1,12 @@ { "device_automation": { "action_type": { - "clean": "La {entity_name} rengj\u00f8res", - "dock": "La {entity_name} tilbake til dock" + "clean": "La {entity_name} rengj\u00f8re", + "dock": "La {entity_name} returnere til dokken" }, "condition_type": { - "is_cleaning": "{entity_name} rengj\u00f8res", - "is_docked": "{entity_name} er docked" + "is_cleaning": "{entity_name} rengj\u00f8r", + "is_docked": "{entity_name} er dokket" }, "trigger_type": { "cleaning": "{entity_name} startet rengj\u00f8ringen", @@ -18,6 +18,10 @@ "cleaning": "Rengj\u00f8ring", "docked": "Dokket", "error": "Feil", + "idle": "Inaktiv", + "off": "Av", + "on": "P\u00e5", + "paused": "Pauset", "returning": "Returner til dokken" } }, diff --git a/homeassistant/components/vacuum/translations/sl.json b/homeassistant/components/vacuum/translations/sl.json index 55ceb336c4f..faa94ef07cf 100644 --- a/homeassistant/components/vacuum/translations/sl.json +++ b/homeassistant/components/vacuum/translations/sl.json @@ -20,9 +20,9 @@ "error": "Napaka", "idle": "V pripravljenosti", "off": "Izklju\u010den", - "on": "Vklju\u010den", - "paused": "Zaustavljeno", - "returning": "Vra\u010dam se na postajo" + "on": "Vklopljen", + "paused": "Na pavzi", + "returning": "Vra\u010danje na postajo" } }, "title": "Sesam" diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 86f4e7a7cd8..8c4cc5a4043 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Velbus Binary Sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import VelbusEntity from .const import DOMAIN @@ -20,7 +20,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class VelbusBinarySensor(VelbusEntity, BinarySensorDevice): +class VelbusBinarySensor(VelbusEntity, BinarySensorEntity): """Representation of a Velbus Binary Sensor.""" @property diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 8810f945ba9..6ef91d65c91 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -3,7 +3,7 @@ import logging from velbus.util import VelbusException -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, @@ -27,7 +27,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class VelbusClimate(VelbusEntity, ClimateDevice): +class VelbusClimate(VelbusEntity, ClimateEntity): """Representation of a Velbus thermostat.""" @property diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 4478bb81c3c..efe4fdc964b 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -9,7 +9,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) from . import VelbusEntity @@ -29,7 +29,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class VelbusCover(VelbusEntity, CoverDevice): +class VelbusCover(VelbusEntity, CoverEntity): """Representation a Velbus cover.""" @property diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index d7654feab2d..4aebbb27953 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_FLASH, SUPPORT_TRANSITION, - Light, + LightEntity, ) from . import VelbusEntity @@ -32,7 +32,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class VelbusLight(VelbusEntity, Light): +class VelbusLight(VelbusEntity, LightEntity): """Representation of a Velbus light.""" @property diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index 64d4b7c17f8..91746b1513e 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -3,7 +3,7 @@ import logging from velbus.util import VelbusException -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import VelbusEntity from .const import DOMAIN @@ -22,7 +22,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class VelbusSwitch(VelbusEntity, SwitchDevice): +class VelbusSwitch(VelbusEntity, SwitchEntity): """Representation of a switch.""" @property diff --git a/homeassistant/components/velbus/translations/ca.json b/homeassistant/components/velbus/translations/ca.json index 4738e236fff..783bb6d7f3b 100644 --- a/homeassistant/components/velbus/translations/ca.json +++ b/homeassistant/components/velbus/translations/ca.json @@ -11,7 +11,7 @@ "user": { "data": { "name": "Nom de la connexi\u00f3 Velbus", - "port": "Cadena de connexi\u00f3" + "port": "String de connexi\u00f3" }, "title": "Tipus de connexi\u00f3 Velbus" } diff --git a/homeassistant/components/velbus/translations/ko.json b/homeassistant/components/velbus/translations/ko.json index 0d8e003472a..98829920bc7 100644 --- a/homeassistant/components/velbus/translations/ko.json +++ b/homeassistant/components/velbus/translations/ko.json @@ -13,7 +13,7 @@ "name": "Velbus \uc5f0\uacb0 \uc774\ub984", "port": "\uc5f0\uacb0 \ubb38\uc790\uc5f4" }, - "title": "Velbus \uc5f0\uacb0 \uc720\ud615 \uc815\uc758" + "title": "Velbus \uc5f0\uacb0 \uc720\ud615 \uc815\uc758\ud558\uae30" } } } diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index fe5b1dcf3af..901946b3245 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -8,7 +8,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, - CoverDevice, + CoverEntity, ) from homeassistant.core import callback @@ -25,7 +25,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class VeluxCover(CoverDevice): +class VeluxCover(CoverEntity): """Representation of a Velux cover.""" def __init__(self, node): @@ -46,6 +46,11 @@ class VeluxCover(CoverDevice): """Store register state change callback.""" self.async_register_callbacks() + @property + def unique_id(self): + """Return the unique ID of this cover.""" + return self.node.serial_number + @property def name(self): """Return the name of the Velux device.""" diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index d67e29af693..e6747060da6 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,6 +2,6 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.12"], + "requirements": ["pyvlx==0.2.14"], "codeowners": ["@Julius2342"] } diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7de6427b5d8..261339f70dd 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -4,7 +4,7 @@ import logging from venstarcolortouch import VenstarColorTouch import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -96,7 +96,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([VenstarThermostat(client, humidifier)], True) -class VenstarThermostat(ClimateDevice): +class VenstarThermostat(ClimateEntity): """Representation of a Venstar thermostat.""" def __init__(self, client, humidifier): diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index c98833a7daa..1e1538420b5 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -25,7 +25,7 @@ from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp from .common import ControllerData, get_configured_platforms -from .config_flow import new_options +from .config_flow import fix_device_id_list, new_options from .const import ( ATTR_CURRENT_ENERGY_KWH, ATTR_CURRENT_POWER_W, @@ -81,17 +81,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ), ) + saved_light_ids = config_entry.options.get(CONF_LIGHTS, []) + saved_exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) + base_url = config_entry.data[CONF_CONTROLLER] - light_ids = config_entry.options.get(CONF_LIGHTS, []) - exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) + light_ids = fix_device_id_list(saved_light_ids) + exclude_ids = fix_device_id_list(saved_exclude_ids) + + # If the ids were corrected. Update the config entry. + if light_ids != saved_light_ids or exclude_ids != saved_exclude_ids: + hass.config_entries.async_update_entry( + entry=config_entry, options=new_options(light_ids, exclude_ids) + ) # Initialize the Vera controller. controller = veraApi.VeraController(base_url) controller.start() hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - lambda event: hass.async_add_executor_job(controller.stop), + EVENT_HOMEASSISTANT_STOP, lambda event: controller.stop() ) try: diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 621dc09930d..557874f846a 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -5,7 +5,7 @@ from typing import Callable, List from homeassistant.components.binary_sensor import ( DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -32,7 +32,7 @@ async def async_setup_entry( ) -class VeraBinarySensor(VeraDevice, BinarySensorDevice): +class VeraBinarySensor(VeraDevice, BinarySensorEntity): """Representation of a Vera Binary Sensor.""" def __init__(self, vera_device, controller): diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 520c3b516df..9b8601e45d1 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -5,7 +5,7 @@ from typing import Callable, List from homeassistant.components.climate import ( DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, - ClimateDevice, + ClimateEntity, ) from homeassistant.components.climate.const import ( FAN_AUTO, @@ -49,7 +49,7 @@ async def async_setup_entry( ) -class VeraThermostat(VeraDevice, ClimateDevice): +class VeraThermostat(VeraDevice, ClimateEntity): """Representation of a Vera Thermostat.""" def __init__(self, vera_device, controller): diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 3d2b30f1079..cac17951cc1 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Vera.""" import logging import re -from typing import List, cast +from typing import Any, List import pyvera as pv from requests.exceptions import RequestException @@ -17,20 +17,22 @@ LIST_REGEX = re.compile("[^0-9]+") _LOGGER = logging.getLogger(__name__) -def str_to_int_list(data: str) -> List[str]: +def fix_device_id_list(data: List[Any]) -> List[int]: + """Fix the id list by converting it to a supported int list.""" + return str_to_int_list(list_to_str(data)) + + +def str_to_int_list(data: str) -> List[int]: """Convert a string to an int list.""" - if isinstance(str, list): - return cast(List[str], data) - - return [s for s in LIST_REGEX.split(data) if len(s) > 0] + return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] -def int_list_to_str(data: List[str]) -> str: +def list_to_str(data: List[Any]) -> str: """Convert an int list to a string.""" return " ".join([str(i) for i in data]) -def new_options(lights: List[str], exclude: List[str]) -> dict: +def new_options(lights: List[int], exclude: List[int]) -> dict: """Create a standard options object.""" return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} @@ -40,10 +42,10 @@ def options_schema(options: dict = None) -> dict: options = options or {} return { vol.Optional( - CONF_LIGHTS, default=int_list_to_str(options.get(CONF_LIGHTS, [])), + CONF_LIGHTS, default=list_to_str(options.get(CONF_LIGHTS, [])), ): str, vol.Optional( - CONF_EXCLUDE, default=int_list_to_str(options.get(CONF_EXCLUDE, [])), + CONF_EXCLUDE, default=list_to_str(options.get(CONF_EXCLUDE, [])), ): str, } diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 0d0edb841c1..a1f536d9cc1 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -6,7 +6,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, - CoverDevice, + CoverEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -33,7 +33,7 @@ async def async_setup_entry( ) -class VeraCover(VeraDevice, CoverDevice): +class VeraCover(VeraDevice, CoverEntity): """Representation a Vera Cover.""" def __init__(self, vera_device, controller): diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 877fdf51f0a..250842f1687 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -9,7 +9,7 @@ from homeassistant.components.light import ( ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -37,7 +37,7 @@ async def async_setup_entry( ) -class VeraLight(VeraDevice, Light): +class VeraLight(VeraDevice, LightEntity): """Representation of a Vera Light, including dimmable.""" def __init__(self, vera_device, controller): diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index da3c432a6af..f85beb5ba69 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -5,7 +5,7 @@ from typing import Callable, List from homeassistant.components.lock import ( DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, - LockDevice, + LockEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -36,7 +36,7 @@ async def async_setup_entry( ) -class VeraLock(VeraDevice, LockDevice): +class VeraLock(VeraDevice, LockEntity): """Representation of a Vera lock.""" def __init__(self, vera_device, controller): diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index a7ae6d45573..0a9a94d6372 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -5,7 +5,7 @@ from typing import Callable, List from homeassistant.components.switch import ( DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, - SwitchDevice, + SwitchEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -33,7 +33,7 @@ async def async_setup_entry( ) -class VeraSwitch(VeraDevice, SwitchDevice): +class VeraSwitch(VeraDevice, SwitchEntity): """Representation of a Vera Switch.""" def __init__(self, vera_device, controller): diff --git a/homeassistant/components/vera/translations/es-419.json b/homeassistant/components/vera/translations/es-419.json new file mode 100644 index 00000000000..243050b1544 --- /dev/null +++ b/homeassistant/components/vera/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Un controlador ya est\u00e1 configurado.", + "cannot_connect": "No se pudo conectar al controlador con la URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Identificadores de dispositivo de Vera para excluir de Home Assistant.", + "lights": "Vera cambia los identificadores del dispositivo para tratarlos como luces en Home Assistant.", + "vera_controller_url": "URL del controlador" + }, + "description": "Proporcione una URL del controlador Vera a continuaci\u00f3n. Deber\u00eda verse as\u00ed: http://192.168.1.161:3480.", + "title": "Configurar el controlador Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Identificadores de dispositivo de Vera para excluir de Home Assistant.", + "lights": "Vera cambia los identificadores del dispositivo para tratarlos como luces en Home Assistant." + }, + "description": "Consulte la documentaci\u00f3n de vera para obtener detalles sobre los par\u00e1metros opcionales: https://www.home-assistant.io/integrations/vera/. Nota: Cualquier cambio aqu\u00ed necesitar\u00e1 reiniciar el servidor del asistente de inicio. Para borrar valores, proporcione un espacio.", + "title": "Opciones de controlador Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/fr.json b/homeassistant/components/vera/translations/fr.json index 4d1b5a7eb6d..9cc6d871dd7 100644 --- a/homeassistant/components/vera/translations/fr.json +++ b/homeassistant/components/vera/translations/fr.json @@ -3,6 +3,21 @@ "abort": { "already_configured": "Un contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9.", "cannot_connect": "Impossible de se connecter au contr\u00f4leur avec l'url {base_url}" + }, + "step": { + "user": { + "data": { + "vera_controller_url": "URL du contr\u00f4leur" + }, + "title": "Configurer le contr\u00f4leur Vera" + } + } + }, + "options": { + "step": { + "init": { + "title": "Options du contr\u00f4leur Vera" + } } } } \ No newline at end of file diff --git a/homeassistant/components/vera/translations/ko.json b/homeassistant/components/vera/translations/ko.json index 6572570916b..56aee4bd767 100644 --- a/homeassistant/components/vera/translations/ko.json +++ b/homeassistant/components/vera/translations/ko.json @@ -12,7 +12,7 @@ "vera_controller_url": "\ucee8\ud2b8\ub864\ub7ec URL" }, "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", - "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc124\uc815" + "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30" } } }, diff --git a/homeassistant/components/vera/translations/nl.json b/homeassistant/components/vera/translations/nl.json new file mode 100644 index 00000000000..358905bd50f --- /dev/null +++ b/homeassistant/components/vera/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Er is al een controller geconfigureerd.", + "cannot_connect": "Kan geen verbinding maken met controller met url {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Vera-apparaat-ID's om uit te sluiten van Home Assistant.", + "lights": "Vera-schakelapparaat id's behandelen als lichten in Home Assistant.", + "vera_controller_url": "Controller-URL" + }, + "description": "Geef hieronder een URL voor de Vera-controller op. Het zou er zo uit moeten zien: http://192.168.1.161:3480.", + "title": "Stel Vera controller in" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera-apparaat-ID's om uit te sluiten van Home Assistant.", + "lights": "Vera-schakelapparaat id's behandelen als lichten in Home Assistant." + }, + "title": "Vera controller opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json index dbc6bca1365..503f9d22547 100644 --- a/homeassistant/components/vera/translations/no.json +++ b/homeassistant/components/vera/translations/no.json @@ -23,7 +23,7 @@ "exclude": "Vera-enhets-ID-er som skal ekskluderes fra Home Assistant.", "lights": "Vera bytter enhets-ID-er for \u00e5 behandle som lys i Home Assistant." }, - "description": "Se vera dokumentasjonen for detaljer om valgfrie parametere: https://www.home-assistant.io/integrations/vera/. Merk: Eventuelle endringer her vil trenge en omstart til home assistant-serveren. For \u00e5 fjerne verdier, gi et rom.", + "description": "Se Vera dokumentasjonen for detaljer om valgfrie parametere: https://www.home-assistant.io/integrations/vera/. Merk: Eventuelle endringer her vil trenge en omstart av Home Assistant-serveren. For \u00e5 fjerne verdier, gi et rom.", "title": "Alternativer for Vera-kontroller" } } diff --git a/homeassistant/components/vera/translations/ru.json b/homeassistant/components/vera/translations/ru.json index 99095dffdfc..f4280cb1c1b 100644 --- a/homeassistant/components/vera/translations/ru.json +++ b/homeassistant/components/vera/translations/ru.json @@ -7,11 +7,11 @@ "step": { "user": { "data": { - "exclude": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant.", - "lights": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u0432 Home Assistant.", + "exclude": "ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera, \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant", + "lights": "ID \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0435\u0439 Vera, \u0434\u043b\u044f \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435", "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430" }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 Vera. \u0410\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'http://192.168.1.161:3480'.", + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.1.161:3480').", "title": "Vera" } } diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 78a09e439d7..1ef3eb442cd 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -40,7 +40,7 @@ def set_arm_state(state, code=None): hub.update_overview(no_throttle=True) -class VerisureAlarm(alarm.AlarmControlPanel): +class VerisureAlarm(alarm.AlarmControlPanelEntity): """Representation of a Verisure alarm status.""" def __init__(self): diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index bbdd9f54e83..206a4072ace 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, - BinarySensorDevice, + BinarySensorEntity, ) from . import CONF_DOOR_WINDOW, HUB as hub @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class VerisureDoorWindowSensor(BinarySensorDevice): +class VerisureDoorWindowSensor(BinarySensorEntity): """Representation of a Verisure door window sensor.""" def __init__(self, device_label): @@ -73,7 +73,7 @@ class VerisureDoorWindowSensor(BinarySensorDevice): hub.update_overview() -class VerisureEthernetStatus(BinarySensorDevice): +class VerisureEthernetStatus(BinarySensorEntity): """Representation of a Verisure VBOX internet status.""" @property diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 5b5d50347ac..96e40c5c36f 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -2,7 +2,7 @@ import logging from time import monotonic, sleep -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from . import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, HUB as hub @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(locks) -class VerisureDoorlock(LockDevice): +class VerisureDoorlock(LockEntity): """Representation of a Verisure doorlock.""" def __init__(self, device_label): diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 2df250303c5..8025b22f402 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -2,7 +2,7 @@ import logging from time import monotonic -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import CONF_SMARTPLUGS, HUB as hub @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class VerisureSmartplug(SwitchDevice): +class VerisureSmartplug(SwitchEntity): """Representation of a Verisure smartplug.""" def __init__(self, device_id): diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py index 4b44cb7aa2a..78eadffea34 100644 --- a/homeassistant/components/versasense/switch.py +++ b/homeassistant/components/versasense/switch.py @@ -1,7 +1,7 @@ """Support for VersaSense actuator peripheral.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DOMAIN from .const import ( @@ -42,7 +42,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(actuator_list) -class VActuator(SwitchDevice): +class VActuator(SwitchEntity): """Representation of an Actuator.""" def __init__(self, peripheral, parent_name, unit, measurement, consumer): diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 80c934c98db..1bdf32e1037 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -3,10 +3,17 @@ "step": { "user": { "title": "Enter Username and Password", - "data": { "username": "Email Address", "password": "Password" } + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, - "error": { "invalid_login": "Invalid username or password" }, - "abort": { "already_setup": "Only one Vesync instance is allowed" } + "error": { + "invalid_login": "Invalid username or password" + }, + "abort": { + "already_setup": "Only one Vesync instance is allowed" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 6ab5c0c4368..fb6e83227e9 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,7 +1,7 @@ """Support for Etekcity VeSync switches.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -55,7 +55,7 @@ def _async_setup_entities(devices, async_add_entities): async_add_entities(dev_list, update_before_add=True) -class VeSyncSwitchHA(VeSyncDevice, SwitchDevice): +class VeSyncSwitchHA(VeSyncDevice, SwitchEntity): """Representation of a VeSync switch.""" def __init__(self, plug): @@ -90,7 +90,7 @@ class VeSyncSwitchHA(VeSyncDevice, SwitchDevice): self.smartplug.update_energy() -class VeSyncLightSwitch(VeSyncDevice, SwitchDevice): +class VeSyncLightSwitch(VeSyncDevice, SwitchEntity): """Handle representation of VeSync Light Switch.""" def __init__(self, switch): diff --git a/homeassistant/components/vesync/translations/ca.json b/homeassistant/components/vesync/translations/ca.json index 6dbf41d9ef2..cdeeaa3ed57 100644 --- a/homeassistant/components/vesync/translations/ca.json +++ b/homeassistant/components/vesync/translations/ca.json @@ -12,7 +12,7 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, - "title": "Introdueix el nom d\u2019usuari i contrasenya" + "title": "Introdueix el nom d'usuari i contrasenya" } } } diff --git a/homeassistant/components/vesync/translations/en.json b/homeassistant/components/vesync/translations/en.json index c109a81aa2f..a7a12160187 100644 --- a/homeassistant/components/vesync/translations/en.json +++ b/homeassistant/components/vesync/translations/en.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "Email Address" + "username": "Email" }, "title": "Enter Username and Password" } diff --git a/homeassistant/components/vesync/translations/ko.json b/homeassistant/components/vesync/translations/ko.json index 20672e018f7..7fbadde9c96 100644 --- a/homeassistant/components/vesync/translations/ko.json +++ b/homeassistant/components/vesync/translations/ko.json @@ -10,9 +10,9 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + "username": "\uc774\uba54\uc77c" }, - "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud558\uae30" } } } diff --git a/homeassistant/components/vesync/translations/no.json b/homeassistant/components/vesync/translations/no.json index 75cfb04b9db..c0624958e87 100644 --- a/homeassistant/components/vesync/translations/no.json +++ b/homeassistant/components/vesync/translations/no.json @@ -12,7 +12,7 @@ "password": "Passord", "username": "E-postadresse" }, - "title": "Skriv inn brukernavn og passord" + "title": "Fyll inn brukernavn og passord" } } } diff --git a/homeassistant/components/vesync/translations/pl.json b/homeassistant/components/vesync/translations/pl.json index aa5d4dc587f..74fbc7c753b 100644 --- a/homeassistant/components/vesync/translations/pl.json +++ b/homeassistant/components/vesync/translations/pl.json @@ -9,8 +9,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 nazw\u0119 u\u017cytkownika i has\u0142o." } diff --git a/homeassistant/components/vesync/translations/zh-Hant.json b/homeassistant/components/vesync/translations/zh-Hant.json index cd7cc7ea91b..efdbee6873c 100644 --- a/homeassistant/components/vesync/translations/zh-Hant.json +++ b/homeassistant/components/vesync/translations/zh-Hant.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u5bc6\u78bc", - "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + "username": "\u96fb\u5b50\u90f5\u4ef6" }, "title": "\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc" } diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 1b101cc7612..ce88ea8e3e7 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -3,7 +3,7 @@ import logging import requests -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -97,7 +97,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ViCareClimate(ClimateDevice): +class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" def __init__(self, name, api, heating_type): diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index eea3d81faf6..c6aa5205f24 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -5,7 +5,7 @@ import requests from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ViCareWater(WaterHeaterDevice): +class ViCareWater(WaterHeaterEntity): """Representation of the ViCare domestic hot water device.""" def __init__(self, name, api, heating_type): diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json index 399e30446e9..6de59a51f96 100644 --- a/homeassistant/components/vilfo/strings.json +++ b/homeassistant/components/vilfo/strings.json @@ -5,8 +5,8 @@ "title": "Connect to the Vilfo Router", "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", "data": { - "host": "Router hostname or IP", - "access_token": "Access token for the Vilfo Router API" + "host": "[%key:common::config_flow::data::host%]", + "access_token": "[%key:common::config_flow::data::access_token%]" } } }, @@ -19,4 +19,4 @@ "already_configured": "This Vilfo Router is already configured." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/ca.json b/homeassistant/components/vilfo/translations/ca.json index 5b8c12bab6c..8bd6185c094 100644 --- a/homeassistant/components/vilfo/translations/ca.json +++ b/homeassistant/components/vilfo/translations/ca.json @@ -5,16 +5,16 @@ }, "error": { "cannot_connect": "No s'ha pogut connectar. Verifica la informaci\u00f3 proporcionada i torna-ho a provar.", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida. Comprova el testimoni d'acc\u00e9s i torna-ho a provar.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida. Comprova el token d'acc\u00e9s i torna-ho a provar.", "unknown": "S'ha produ\u00eft un error inesperat durant la configuraci\u00f3 de la integraci\u00f3." }, "step": { "user": { "data": { - "access_token": "Testimoni d'acc\u00e9s per l'API de l'encaminador Vilfo", + "access_token": "Token d'acc\u00e9s per l'API de l'encaminador Vilfo", "host": "Nom d'amfitri\u00f3 o IP de l'encaminador" }, - "description": "Configura la integraci\u00f3 de l'encaminador Vilfo. Necessites la seva IP o nom d'amfitri\u00f3 i el testimoni d'acc\u00e9s de l'API (token). Per a m\u00e9s informaci\u00f3, visita: https://www.home-assistant.io/integrations/vilfo", + "description": "Configura la integraci\u00f3 de l'encaminador Vilfo. Necessites la seva IP o nom d'amfitri\u00f3 i el token d'acc\u00e9s de l'API. Per a m\u00e9s informaci\u00f3, visita: https://www.home-assistant.io/integrations/vilfo", "title": "Connexi\u00f3 amb l'encaminador Vilfo" } } diff --git a/homeassistant/components/vilfo/translations/en.json b/homeassistant/components/vilfo/translations/en.json index 49a94414403..57815aa0393 100644 --- a/homeassistant/components/vilfo/translations/en.json +++ b/homeassistant/components/vilfo/translations/en.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "access_token": "Access token for the Vilfo Router API", - "host": "Router hostname or IP" + "access_token": "Access Token", + "host": "Host" }, "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", "title": "Connect to the Vilfo Router" diff --git a/homeassistant/components/vilfo/translations/es-419.json b/homeassistant/components/vilfo/translations/es-419.json new file mode 100644 index 00000000000..9c62c327235 --- /dev/null +++ b/homeassistant/components/vilfo/translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Este enrutador Vilfo ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar. Verifique la informaci\u00f3n que proporcion\u00f3 e intente nuevamente.", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida Verifique el token de acceso e intente nuevamente.", + "unknown": "Se produjo un error inesperado al configurar la integraci\u00f3n." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso para la API del enrutador Vilfo", + "host": "Nombre de host o IP del enrutador" + }, + "description": "Configure la integraci\u00f3n del enrutador Vilfo. Necesita su nombre de host/IP de Vilfo Router y un token de acceso API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo", + "title": "Conectar con el Router Vilfo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json index 272711789d8..867b78ac411 100644 --- a/homeassistant/components/vilfo/translations/fr.json +++ b/homeassistant/components/vilfo/translations/fr.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "host": "Nom d'h\u00f4te ou IP du routeur" + }, "title": "Connectez-vous au routeur Vilfo" } } diff --git a/homeassistant/components/vilfo/translations/ko.json b/homeassistant/components/vilfo/translations/ko.json index f0b96eb77b7..70a315ae703 100644 --- a/homeassistant/components/vilfo/translations/ko.json +++ b/homeassistant/components/vilfo/translations/ko.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "access_token": "Vilfo \ub77c\uc6b0\ud130 API \uc6a9 \uc561\uc138\uc2a4 \ud1a0\ud070", - "host": "\ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c" + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "host": "\ud638\uc2a4\ud2b8" }, "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 / IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Vilfo \ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/vilfo/translations/no.json b/homeassistant/components/vilfo/translations/no.json index 36c6e79989b..515e8f29d21 100644 --- a/homeassistant/components/vilfo/translations/no.json +++ b/homeassistant/components/vilfo/translations/no.json @@ -14,7 +14,7 @@ "access_token": "Tilgangstoken for Vilfo Router API", "host": "Ruter vertsnavn eller IP" }, - "description": "Konfigurer Vilfo Router-integreringen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. Hvis du vil ha mer informasjon om denne integreringen og hvordan du f\u00e5r disse detaljene, kan du g\u00e5 til: https://www.home-assistant.io/integrations/vilfo", + "description": "Sett opp Vilfo Router-integrasjonen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. For ytterligere informasjon om denne integrasjonen og hvordan du f\u00e5r disse detaljene, bes\u00f8k [https://www.home-assistant.io/integrations/vilfo](https://www.home-assistant.io/integrations/vilfo)", "title": "Koble til Vilfo Ruteren" } } diff --git a/homeassistant/components/vilfo/translations/pl.json b/homeassistant/components/vilfo/translations/pl.json index 4cff9fb6fea..939d840035f 100644 --- a/homeassistant/components/vilfo/translations/pl.json +++ b/homeassistant/components/vilfo/translations/pl.json @@ -6,13 +6,13 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a wprowadzone dane i spr\u00f3buj ponownie.", "invalid_auth": "Nieudane uwierzytelnienie. Sprawd\u017a token dost\u0119pu i spr\u00f3buj ponownie.", - "unknown": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas konfiguracji integracji." + "unknown": "[%key_id:common::config_flow::error::unknown%]" }, "step": { "user": { "data": { "access_token": "Token dost\u0119pu do interfejsu API routera Vilfo", - "host": "Nazwa hosta lub adres IP routera" + "host": "[%key_id:common::config_flow::data::host%]" }, "description": "Skonfiguruj integracj\u0119 routera Vilfo. Potrzebujesz nazwy hosta/adresu IP routera Vilfo i tokena dost\u0119pu do interfejsu API. Aby uzyska\u0107 dodatkowe informacje na temat tej integracji i sposobu uzyskania niezb\u0119dnych danych do konfiguracji, odwied\u017a: https://www.home-assistant.io/integrations/vilfo", "title": "Po\u0142\u0105czenie z routerem Vilfo" diff --git a/homeassistant/components/vilfo/translations/ru.json b/homeassistant/components/vilfo/translations/ru.json index 372c9750410..329a5e400e5 100644 --- a/homeassistant/components/vilfo/translations/ru.json +++ b/homeassistant/components/vilfo/translations/ru.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a API \u0440\u043e\u0443\u0442\u0435\u0440\u0430", - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "host": "\u0425\u043e\u0441\u0442" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 API. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", "title": "Vilfo Router" diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json index 665474e2d76..c1fe87f65e8 100644 --- a/homeassistant/components/vilfo/translations/zh-Hant.json +++ b/homeassistant/components/vilfo/translations/zh-Hant.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u9470", - "host": "\u8def\u7531\u5668\u4e3b\u6a5f\u7aef\u6216 IP \u4f4d\u5740" + "access_token": "\u5b58\u53d6\u5bc6\u9470", + "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u5bc6\u9470\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo", "title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668" diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 022a5ea35f1..67bb3d3633c 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -10,7 +10,7 @@ from pyvizio.const import APP_HOME, APPS, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_AP from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, SUPPORT_SELECT_SOUND_MODE, - MediaPlayerDevice, + MediaPlayerEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -114,7 +114,7 @@ async def async_setup_entry( async_add_entities([entity], update_before_add=True) -class VizioDevice(MediaPlayerDevice): +class VizioDevice(MediaPlayerEntity): """Media Player implementation which performs REST requests to device.""" def __init__( diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 3a9766c207b..9cceb6c35a2 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -3,18 +3,20 @@ "step": { "user": { "title": "Setup VIZIO SmartCast Device", - "description": "An Access Token is only needed for TVs. If you are configuring a TV and do not have an Access Token yet, leave it blank to go through a pairing process.", + "description": "An [%key:common::config_flow::data::access_token%] is only needed for TVs. If you are configuring a TV and do not have an [%key:common::config_flow::data::access_token%] yet, leave it blank to go through a pairing process.", "data": { "name": "Name", - "host": ":", + "host": "[%key:common::config_flow::data::host%]", "device_class": "Device Type", - "access_token": "Access Token" + "access_token": "[%key:common::config_flow::data::access_token%]" } }, "pair_tv": { "title": "Complete Pairing Process", "description": "Your TV should be displaying a code. Enter that code into the form and then continue to the next step to complete the pairing.", - "data": { "pin": "PIN" } + "data": { + "pin": "PIN" + } }, "pairing_complete": { "title": "Pairing Complete", @@ -22,14 +24,14 @@ }, "pairing_complete_import": { "title": "Pairing Complete", - "description": "Your VIZIO SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'." + "description": "Your VIZIO SmartCast TV is now connected to Home Assistant.\n\nYour [%key:common::config_flow::data::access_token%] is '**{access_token}**'." } }, "error": { "host_exists": "VIZIO device with specified host already configured.", "name_exists": "VIZIO device with specified name already configured.", - "complete_pairing failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", - "cant_connect": "Could not connect to the device. [Review the docs](https://www.home-assistant.io/integrations/vizio/) and re-verify that:\n- The device is powered on\n- The device is connected to the network\n- The values you filled in are accurate\nbefore attempting to resubmit." + "complete_pairing_failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", + "cant_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "already_setup": "This entry has already been setup.", @@ -49,4 +51,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json index b59346bcf42..2194f33dfff 100644 --- a/homeassistant/components/vizio/translations/ca.json +++ b/homeassistant/components/vizio/translations/ca.json @@ -5,8 +5,9 @@ "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat." }, "error": { - "cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d\u2019intentar tornar a presentar.", + "cant_connect": "No s'ha pogut connectar", "complete_pairing failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", + "complete_pairing_failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", "host_exists": "Dispositiu Vizio amb aquest nom d'amfitri\u00f3 ja configurat.", "name_exists": "Dispositiu Vizio amb aquest nom ja configurat." }, @@ -23,17 +24,17 @@ "title": "Emparellament completat" }, "pairing_complete_import": { - "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant.\n\nEl teu testimoni d'acc\u00e9s (access token) \u00e9s '**{access_token}**'.", + "description": "El dispositiu VIZIO SmartCast TV est\u00e0 connectat a Home Assistant.\n\nEl teu [%key::common::config_flow::data::access_token%] \u00e9s '**{access_token}**'.", "title": "Emparellament completat" }, "user": { "data": { - "access_token": "Testimoni d'acc\u00e9s", + "access_token": "[%key::common::config_flow::data::access_token%]", "device_class": "Tipus de dispositiu", "host": ":", "name": "Nom" }, - "description": "Nom\u00e9s es necessita testimoni d'acc\u00e9s per als televisors. Si est\u00e0s configurant un televisor i encara no tens un testimoni d'acc\u00e9s, deixa-ho en blanc per poder fer el proc\u00e9s d'emparellament.", + "description": "Nom\u00e9s es necessita el [%key::common::config_flow::data::access_token%] per als televisors. Si est\u00e0s configurant un televisor i encara no tens un [%key::common::config_flow::data::access_token%], deixa-ho en blanc per poder fer el proc\u00e9s d'emparellament.", "title": "Configuraci\u00f3 del client de Vizio SmartCast" } } diff --git a/homeassistant/components/vizio/translations/en.json b/homeassistant/components/vizio/translations/en.json index d160356cb07..d09e29037ee 100644 --- a/homeassistant/components/vizio/translations/en.json +++ b/homeassistant/components/vizio/translations/en.json @@ -5,8 +5,9 @@ "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." }, "error": { - "cant_connect": "Could not connect to the device. [Review the docs](https://www.home-assistant.io/integrations/vizio/) and re-verify that:\n- The device is powered on\n- The device is connected to the network\n- The values you filled in are accurate\nbefore attempting to resubmit.", + "cant_connect": "Failed to connect", "complete_pairing failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", + "complete_pairing_failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", "host_exists": "VIZIO device with specified host already configured.", "name_exists": "VIZIO device with specified name already configured." }, @@ -30,7 +31,7 @@ "data": { "access_token": "Access Token", "device_class": "Device Type", - "host": ":", + "host": "Host", "name": "Name" }, "description": "An Access Token is only needed for TVs. If you are configuring a TV and do not have an Access Token yet, leave it blank to go through a pairing process.", diff --git a/homeassistant/components/vizio/translations/es-419.json b/homeassistant/components/vizio/translations/es-419.json new file mode 100644 index 00000000000..d60f839d653 --- /dev/null +++ b/homeassistant/components/vizio/translations/es-419.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_setup": "Esta entrada ya se ha configurado.", + "updated_entry": "Esta entrada ya se configur\u00f3, pero el nombre, las aplicaciones o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n importada anteriormente, por lo que la entrada de configuraci\u00f3n se actualiz\u00f3 en consecuencia." + }, + "error": { + "cant_connect": "No se pudo conectar al dispositivo. [Revise los documentos] (https://www.home-assistant.io/integrations/vizio/) y vuelva a verificar que: \n - El dispositivo est\u00e1 encendido \n - El dispositivo est\u00e1 conectado a la red. \n - Los valores que complet\u00f3 son precisos \n antes de intentar volver a enviar.", + "complete_pairing failed": "No se puede completar el emparejamiento. Aseg\u00farese de que el PIN que proporcion\u00f3 sea correcto y que el televisor siga encendido y conectado a la red antes de volver a enviarlo.", + "host_exists": "Dispositivo VIZIO con el host especificado ya configurado.", + "name_exists": "Dispositivo VIZIO con el nombre especificado ya configurado." + }, + "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "description": "Su televisi\u00f3n debe mostrar un c\u00f3digo. Ingrese ese c\u00f3digo en el formulario y luego contin\u00fae con el siguiente paso para completar el emparejamiento.", + "title": "Proceso de emparejamiento completo" + }, + "pairing_complete": { + "description": "Su dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", + "title": "Emparejamiento completo" + }, + "pairing_complete_import": { + "description": "Su VIZIO SmartCast TV ahora est\u00e1 conectado a Home Assistant. \n\nSu token de acceso es '** {access_token} **'.", + "title": "Emparejamiento completo" + }, + "user": { + "data": { + "access_token": "Token de acceso", + "device_class": "Tipo de dispositivo", + "host": ":", + "name": "Nombre" + }, + "description": "Solo se necesita un token de acceso para televisores. Si est\u00e1 configurando un televisor y a\u00fan no tiene un token de acceso, d\u00e9jelo en blanco para realizar un proceso de emparejamiento.", + "title": "Configurar el dispositivo VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", + "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?", + "volume_step": "Tama\u00f1o de incremento de volumen" + }, + "description": "Si tiene un Smart TV, opcionalmente puede filtrar su lista de origen eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de origen.", + "title": "Actualizar las opciones de VIZIO SmartCast" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index f6cf42ebe99..41246de7531 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -5,8 +5,9 @@ "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." }, "error": { - "cant_connect": "No se pudo conectar al dispositivo. [Revise los documentos] (https://www.home-assistant.io/integrations/vizio/) y vuelva a verificar que:\n- El dispositivo est\u00e1 encendido\n- El dispositivo est\u00e1 conectado a la red\n- Los valores que ha rellenado son precisos\nantes de intentar volver a enviar.", + "cant_connect": "Error al conectar", "complete_pairing failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", + "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", "host_exists": "El host ya est\u00e1 configurado.", "name_exists": "Nombre ya configurado." }, @@ -19,11 +20,11 @@ "title": "Completar Proceso de Emparejamiento" }, "pairing_complete": { - "description": "Tu dispositivo Vizio SmartCast est\u00e1 ahora conectado a Home Assistant.", + "description": "El dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", "title": "Emparejamiento Completado" }, "pairing_complete_import": { - "description": "Su dispositivo Vizio SmartCast TV ahora est\u00e1 conectado a Home Assistant.\n\nEl Token de Acceso es '**{access_token}**'.", + "description": "El dispositivo VIZIO SmartCast TV ahora est\u00e1 conectado a Home Assistant.\n\nEl token de acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, "user": { @@ -33,7 +34,7 @@ "host": "< Host / IP > : ", "name": "Nombre" }, - "description": "Todos los campos son obligatorios excepto el Token de Acceso. Si decides no proporcionar un Token de Acceso y tu Tipo de Dispositivo es \"tv\", se te llevar\u00e1 por un proceso de emparejamiento con tu dispositivo para que se pueda recuperar un Token de Acceso.\n\nPara pasar por el proceso de emparejamiento, antes de pulsar en Enviar, aseg\u00farese de que tu TV est\u00e9 encendida y conectada a la red. Tambi\u00e9n es necesario poder ver la pantalla.", + "description": "El token de acceso solo se necesita para las televisiones. Si est\u00e1s configurando una televisi\u00f3n y a\u00fan no tienes un token de acceso, d\u00e9jalo en blanco para iniciar el proceso de sincronizaci\u00f3n.", "title": "Configurar el cliente de Vizio SmartCast" } } diff --git a/homeassistant/components/vizio/translations/fr.json b/homeassistant/components/vizio/translations/fr.json index 043a33734dc..b28b9881024 100644 --- a/homeassistant/components/vizio/translations/fr.json +++ b/homeassistant/components/vizio/translations/fr.json @@ -15,6 +15,7 @@ "data": { "pin": "PIN" }, + "description": "Votre t\u00e9l\u00e9viseur devrait afficher un code. Saisissez ce code dans le formulaire, puis passez \u00e0 l'\u00e9tape suivante pour terminer le couplage.", "title": "Processus de couplage complet" }, "pairing_complete": { diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index 3bfc872f0e4..0212b8a1281 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -5,8 +5,9 @@ "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome, le app e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza." }, "error": { - "cant_connect": "Impossibile connettersi al dispositivo. [Esamina i documenti] (https://www.home-assistant.io/integrations/vizio/) e verifica nuovamente che: \n - Il dispositivo sia acceso \n - Il dispositivo sia collegato alla rete \n - I valori inseriti siano corretti \n prima di ritentare.", + "cant_connect": "Impossibile connettersi", "complete_pairing failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che il televisore sia ancora alimentato e connesso alla rete prima di inviarlo di nuovo.", + "complete_pairing_failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che la TV sia ancora accesa e collegata alla rete prima di inviare nuovamente.", "host_exists": "Dispositivo VIZIO con host specificato gi\u00e0 configurato.", "name_exists": "Dispositivo VIZIO con il nome specificato gi\u00e0 configurato." }, @@ -23,7 +24,7 @@ "title": "Associazione completata" }, "pairing_complete_import": { - "description": "Il dispositivo VIZIO SmartCast TV \u00e8 ora connesso a Home Assistant. \n\nIl tuo Token di Accesso \u00e8 '**{access_token}**'.", + "description": "Il dispositivo VIZIO SmartCast TV \u00e8 ora connesso a Home Assistant. \n\nIl tuo Token di accesso \u00e8 '**{access_token}**'.", "title": "Associazione completata" }, "user": { @@ -33,7 +34,7 @@ "host": "< Host / IP >: ", "name": "Nome" }, - "description": "Un Token di Accesso \u00e8 necessario solo per i televisori. Se si sta configurando un televisore e non si dispone ancora di un Token di Accesso, lasciarlo vuoto per passare attraverso un processo di associazione.", + "description": "Un Token di accesso \u00e8 necessario solo per i televisori. Se si sta configurando un televisore e non si dispone ancora di un Token di accesso, lasciarlo vuoto per passare attraverso un processo di associazione.", "title": "Configurazione del dispositivo SmartCast VIZIO" } } diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json index 16ad1884294..db690798908 100644 --- a/homeassistant/components/vizio/translations/ko.json +++ b/homeassistant/components/vizio/translations/ko.json @@ -5,10 +5,11 @@ "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "cant_connect": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. [\uc548\ub0b4\ub97c \ucc38\uace0] (https://www.home-assistant.io/integrations/vizio/)\ud558\uace0 \uc591\uc2dd\uc744 \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \ub2e4\uc74c\uc744 \ub2e4\uc2dc \ud655\uc778\ud574\uc8fc\uc138\uc694.\n- \uae30\uae30 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uc2b5\ub2c8\uae4c?\n- \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc2b5\ub2c8\uae4c?\n- \uc785\ub825\ud55c \ub0b4\uc6a9\uc774 \uc62c\ubc14\ub985\ub2c8\uae4c?", + "cant_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "complete_pairing failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc74c \uacfc\uc815\uc744 \uc9c4\ud589\ud558\uae30 \uc804\uc5d0 TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0, TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 VIZIO \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 VIZIO \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { "pair_tv": { @@ -16,25 +17,25 @@ "pin": "PIN" }, "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc591\uc2dd\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", - "title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \uc644\ub8cc" + "title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \ub05d\ub0b4\uae30" }, "pairing_complete": { - "description": "Vizio SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" }, "pairing_complete_import": { - "description": "Vizio SmartCast TV \uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.", + "description": "VIZIO SmartCast TV \uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.", "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" }, "user": { "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", "device_class": "\uae30\uae30 \uc885\ub958", - "host": "<\ud638\uc2a4\ud2b8/ip>:", + "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" }, "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV \uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV \ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.", - "title": "Vizio SmartCast \uae30\uae30 \uc124\uc815" + "title": "VIZIO SmartCast \uae30\uae30 \uc124\uc815\ud558\uae30" } } }, @@ -47,7 +48,7 @@ "volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30" }, "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "Vizo SmartCast \uc635\uc158 \uc5c5\ub370\uc774\ud2b8" + "title": "VIZIO SmartCast \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" } } } diff --git a/homeassistant/components/vizio/translations/lb.json b/homeassistant/components/vizio/translations/lb.json index 48267a23126..8a799834b3a 100644 --- a/homeassistant/components/vizio/translations/lb.json +++ b/homeassistant/components/vizio/translations/lb.json @@ -7,6 +7,7 @@ "error": { "cant_connect": "Konnt sech net mam Apparat verbannen. [Iwwerpr\u00e9ift Dokumentatioun] (https://www.home-assistant.io/integrations/vizio/) a stellt s\u00e9cher dass:\n- Den Apparat ass un\n- Den Apparat ass mam Netzwierk verbonnen\n- D'Optiounen d\u00e9i dir aginn hutt si korrekt\nier dir d'Verbindung nees prob\u00e9iert", "complete_pairing failed": "Feeler beim ofschl\u00e9isse vun der Kopplung. Iwwerpr\u00e9if dass de PIN korrekt an da de Fernsee nach \u00ebmmer ugeschalt a mam Netzwierk verbonnen ass ier de n\u00e4chste Versuch gestart g\u00ebtt.", + "complete_pairing_failed": "Feeler beim ofschl\u00e9isse vun der Kopplung. Iwwerpr\u00e9if dass de PIN korrekt an da de Fernsee nach \u00ebmmer ugeschalt a mam Netzwierk verbonnen ass ier de n\u00e4chste Versuch gestart g\u00ebtt.", "host_exists": "VIZIO Apparat mat d\u00ebsem Host ass scho konfigur\u00e9iert.", "name_exists": "VIZIO Apparat mat d\u00ebsen Numm ass scho konfigur\u00e9iert." }, diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index e1789347bf2..33777175c3e 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -10,6 +10,18 @@ "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "title": "Voltooi het koppelingsproces" + }, + "pairing_complete": { + "title": "Koppelen voltooid" + }, + "pairing_complete_import": { + "title": "Koppelen voltooid" + }, "user": { "data": { "access_token": "Toegangstoken", @@ -17,6 +29,7 @@ "host": ":", "name": "Naam" }, + "description": "Een toegangstoken is alleen nodig voor tv's. Als u een TV configureert en nog geen toegangstoken heeft, laat dit dan leeg en doorloop het koppelingsproces.", "title": "Vizio SmartCast Client instellen" } } @@ -25,8 +38,11 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Apps om op te nemen of uit te sluiten", + "include_or_exclude": "Apps opnemen of uitsluiten?", "volume_step": "Volume Stapgrootte" }, + "description": "Als je een Smart TV hebt, kun je optioneel je bronnenlijst filteren door te kiezen welke apps je in je bronnenlijst wilt opnemen of uitsluiten.", "title": "Update Vizo SmartCast Opties" } } diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index 16a8c6c392e..d7446a415b6 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -5,8 +5,9 @@ "updated_entry": "Dette innlegget har allerede v\u00e6rt oppsett, men navnet, apps, og/eller alternativer som er definert i konfigurasjon som ikke stemmer med det som tidligere er importert konfigurasjon, s\u00e5 konfigurasjonen innlegget har blitt oppdatert i henhold til dette." }, "error": { - "cant_connect": "Kunne ikke koble til enheten. [Se gjennom dokumentene] (https://www.home-assistant.io/integrations/vizio/) og bekreft at: \n - Enheten er sl\u00e5tt p\u00e5 \n - Enheten er koblet til nettverket \n - Verdiene du fylte ut er n\u00f8yaktige \n f\u00f8r du pr\u00f8ver \u00e5 sende inn p\u00e5 nytt.", + "cant_connect": "Kunne ikke koble til enheten. [Se gjennom dokumentene](https://www.home-assistant.io/integrations/vizio/) og bekreft at: \n - Enheten er sl\u00e5tt p\u00e5 \n - Enheten er koblet til nettverket \n - Verdiene du fylte ut er n\u00f8yaktige \n f\u00f8r du pr\u00f8ver \u00e5 sende inn p\u00e5 nytt.", "complete_pairing failed": "Kan ikke fullf\u00f8re sammenkoblingen. Forsikre deg om at PIN-koden du oppga er riktig, og at TV-en fortsatt er p\u00e5 og tilkoblet nettverket f\u00f8r du sender inn p\u00e5 nytt.", + "complete_pairing_failed": "Kan ikke fullf\u00f8re sammenkoblingen. Forsikre deg om at PIN-koden du oppga er riktig, og at TV-en fortsatt er p\u00e5 og tilkoblet nettverket f\u00f8r du sender inn p\u00e5 nytt.", "host_exists": "VIZIO-enhet med spesifisert vert allerede konfigurert.", "name_exists": "VIZIO-enhet med spesifisert navn allerede konfigurert." }, @@ -15,25 +16,25 @@ "data": { "pin": "PIN" }, - "description": "TVen skal vise en kode. Skriv inn denne koden i skjemaet, og fortsett deretter til neste trinn for \u00e5 fullf\u00f8re paringen.", - "title": "Fullf\u00f8r Sammenkoblings Prosessen" + "description": "TVen skal vise en kode. Fyll inn denne koden i skjemaet, og fortsett deretter til neste trinn for \u00e5 fullf\u00f8re paringen.", + "title": "Fullf\u00f8r sammenkoblingsprosess" }, "pairing_complete": { - "description": "Din VIZIO SmartCast enheten er n\u00e5 koblet til Hjemme-Assistent.", - "title": "Sammenkoblingen Er Fullf\u00f8rt" + "description": "Din VIZIO SmartCast enheten er n\u00e5 koblet til Home Assistant.", + "title": "Sammenkoblingen fullf\u00f8rt" }, "pairing_complete_import": { - "description": "VIZIO SmartCast TV er n\u00e5 koblet til Hjemmeassistent.\n\nTilgangstokenet er **{access_token}**.", - "title": "Sammenkoblingen Er Fullf\u00f8rt" + "description": "VIZIO SmartCast TV er n\u00e5 koblet til Home Assistant\n\nTilgangstokenet er '**{access_token}**'.", + "title": "Sammenkoblingen fullf\u00f8rt" }, "user": { "data": { "access_token": "Tilgangstoken", "device_class": "Enhetstype", - "host": ":", + "host": "Vert", "name": "Navn" }, - "description": "En tilgangstoken er bare n\u00f8dvendig for TV-er. Hvis du konfigurerer en TV og ikke har tilgangstoken enn\u00e5, m\u00e5 du la den st\u00e5 tom for \u00e5 g\u00e5 gjennom en sammenkoblingsprosess.", + "description": "En tilgangstoken er bare n\u00f8dvendig for TV-er. Hvis du konfigurerer en TV og ikke har tilgangstoken enda, m\u00e5 du la den st\u00e5 tom for \u00e5 g\u00e5 gjennom en sammenkoblingsprosess.", "title": "Konfigurer VIZIO SmartCast-enhet" } } diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 2d586bc3e36..699ffdbc437 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -23,17 +23,17 @@ "title": "Parowanie zako\u0144czone" }, "pairing_complete_import": { - "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistant'em.\n\nTw\u00f3j token dost\u0119powy to '**{access_token}**'.", + "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistant'em.\n\nToken dost\u0119pu to '**{access_token}**'.", "title": "Parowanie zako\u0144czone" }, "user": { "data": { - "access_token": "Token dost\u0119pu", + "access_token": "[%key_id:common::config_flow::data::access_token%]", "device_class": "Typ urz\u0105dzenia", "host": ":", "name": "Nazwa" }, - "description": "Token dost\u0119powy potrzebny jest tylko dla telewizor\u00f3w. Je\u015bli konfigurujesz telewizor i nie masz jeszcze tokenu dost\u0119powego, pozostaw go pusty aby przej\u015b\u0107 przez proces parowania.", + "description": "Token dost\u0119pu potrzebny jest tylko dla telewizor\u00f3w. Je\u015bli konfigurujesz telewizor i nie masz jeszcze tokenu dost\u0119pu, pozostaw go pusty, aby przej\u015b\u0107 przez proces parowania.", "title": "Konfiguracja klienta Vizio SmartCast" } } diff --git a/homeassistant/components/vizio/translations/ru.json b/homeassistant/components/vizio/translations/ru.json index 62b75b4714a..20d051813c5 100644 --- a/homeassistant/components/vizio/translations/ru.json +++ b/homeassistant/components/vizio/translations/ru.json @@ -5,8 +5,9 @@ "updated_entry": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { - "cant_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e:\n- \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e;\n- \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0441\u0435\u0442\u0438;\n- \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0432\u0432\u0435\u043b\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/integrations/vizio/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "cant_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "complete_pairing failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438.", + "complete_pairing_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438.", "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, @@ -23,17 +24,17 @@ "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e" }, "pairing_complete_import": { - "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e VIZIO SmartCast TV \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a Home Assistant. \n\n\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 - '**{access_token}**'.", + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e VIZIO SmartCast TV \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a Home Assistant. \n\n\u0412\u0430\u0448 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 - '**{access_token}**'.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e" }, "user": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", "device_class": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "host": "<\u0425\u043e\u0441\u0442/IP>:<\u041f\u043e\u0440\u0442>", + "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432. \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u0443 \u0412\u0430\u0441 \u0435\u0449\u0435 \u043d\u0435\u0442 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432. \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443 \u0412\u0430\u0441 \u0435\u0449\u0435 \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", "title": "VIZIO SmartCast" } } diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index db2f4ca9447..a3508481a1b 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -5,8 +5,9 @@ "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { - "cant_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u8a2d\u5099\u3002[\u8acb\u53c3\u8003\u8aaa\u660e\u6587\u4ef6](https://www.home-assistant.io/integrations/vizio/) \u4e26\u78ba\u8a8d\u4ee5\u4e0b\u9805\u76ee\uff1a\n- \u8a2d\u5099\u5df2\u958b\u6a5f\n- \u8a2d\u5099\u5df2\u9023\u7dda\u81f3\u7db2\u8def\n- \u586b\u5beb\u8cc7\u6599\u6b63\u78ba\n\u7136\u5f8c\u518d\u91cd\u65b0\u50b3\u9001\u3002", + "cant_connect": "\u9023\u7dda\u5931\u6557", "complete_pairing failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", + "complete_pairing_failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b VIZIO \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", "name_exists": "\u4f9d\u540d\u7a31\u4e4b VIZIO \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002" }, @@ -23,17 +24,17 @@ "title": "\u914d\u5c0d\u5b8c\u6210" }, "pairing_complete_import": { - "description": "VIZIO SmartCast TV \u8a2d\u5099\u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba\u300c**{access_token}**\u300d\u3002", + "description": "VIZIO SmartCast TV \u8a2d\u5099\u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "user": { "data": { "access_token": "\u5b58\u53d6\u5bc6\u9470", "device_class": "\u8a2d\u5099\u985e\u5225", - "host": "<\u4e3b\u6a5f\u7aef/IP>:", + "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u3002\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5bc6\u9470\uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", + "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u5bc6\u9470 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", "title": "\u8a2d\u5b9a VIZIO SmartCast \u8a2d\u5099" } } diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index c7a3d49fabc..fe387dd4adc 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -4,7 +4,7 @@ import logging import vlc import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class VlcDevice(MediaPlayerDevice): +class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" def __init__(self, name, arguments): diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 45b0971ad9f..1f0d62b6ee8 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -4,7 +4,7 @@ import logging from python_telnet_vlc import ConnectionError as ConnErr, VLCTelnet import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, @@ -76,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class VlcDevice(MediaPlayerDevice): +class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" def __init__(self, name, host, port, passwd): diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 6d46b6015e0..82114cda68b 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -11,7 +11,7 @@ import socket import aiohttp import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, @@ -104,7 +104,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([entity]) -class Volumio(MediaPlayerDevice): +class Volumio(MediaPlayerEntity): """Volumio Player Object.""" def __init__(self, name, host, port, hass): @@ -144,11 +144,7 @@ class Volumio(MediaPlayerDevice): ) return False - try: - return data - except AttributeError: - _LOGGER.error("Received invalid response: %s", data) - return False + return data async def async_update(self): """Update state.""" diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index 20b07bba940..0dba6186ef9 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -1,7 +1,7 @@ """Support for VOC.""" import logging -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from . import DATA_KEY, VolvoEntity @@ -15,7 +15,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)]) -class VolvoSensor(VolvoEntity, BinarySensorDevice): +class VolvoSensor(VolvoEntity, BinarySensorEntity): """Representation of a Volvo sensor.""" @property diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index 2319674cd43..efb8423777b 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -1,7 +1,7 @@ """Support for Volvo On Call locks.""" import logging -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from . import DATA_KEY, VolvoEntity @@ -16,7 +16,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([VolvoLock(hass.data[DATA_KEY], *discovery_info)]) -class VolvoLock(VolvoEntity, LockDevice): +class VolvoLock(VolvoEntity, LockEntity): """Represents a car lock.""" @property diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index 6b5b9bbd0f8..c1b60479e7a 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([VultrBinarySensor(vultr, subscription, name)], True) -class VultrBinarySensor(BinarySensorDevice): +class VultrBinarySensor(BinarySensorEntity): """Representation of a Vultr subscription sensor.""" def __init__(self, vultr, subscription, name): diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py index da79652ec5b..a9c43717a71 100644 --- a/homeassistant/components/vultr/switch.py +++ b/homeassistant/components/vultr/switch.py @@ -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 @@ -50,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([VultrSwitch(vultr, subscription, name)], True) -class VultrSwitch(SwitchDevice): +class VultrSwitch(SwitchEntity): """Representation of a Vultr subscription switch.""" def __init__(self, vultr, subscription, name): diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 16239e0c98c..35b52a1932d 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -7,7 +7,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_DEVICES, CONF_NAME from homeassistant.core import callback @@ -64,7 +64,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): add_entities(binary_sensors) -class W800rf32BinarySensor(BinarySensorDevice): +class W800rf32BinarySensor(BinarySensorEntity): """A representation of a w800rf32 binary sensor.""" def __init__(self, device_id, name, device_class=None, off_delay=None): diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 8200a0309fa..e3af8f146f1 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -6,7 +6,7 @@ import subprocess as sp import voluptuous as vol import wakeonlan -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class WolSwitch(SwitchDevice): +class WolSwitch(SwitchEntity): """Representation of a wake on lan switch.""" def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 4de0a58a881..0763c552075 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -128,7 +128,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class WaterHeaterDevice(Entity): +class WaterHeaterEntity(Entity): """Representation of a water_heater device.""" @property @@ -319,3 +319,15 @@ async def async_service_temperature_set(entity, service): kwargs[value] = temp await entity.async_set_temperature(**kwargs) + + +class WaterHeaterDevice(WaterHeaterEntity): + """Representation of a water heater (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "WaterHeaterDevice is deprecated, modify %s to extend WaterHeaterEntity", + cls.__name__, + ) diff --git a/homeassistant/components/weather/translations/it.json b/homeassistant/components/weather/translations/it.json index 2345dc16eb3..171b29673cd 100644 --- a/homeassistant/components/weather/translations/it.json +++ b/homeassistant/components/weather/translations/it.json @@ -15,7 +15,7 @@ "snowy-rainy": "Nevoso, piovoso", "sunny": "Soleggiato", "windy": "Ventoso", - "windy-variant": "Ventoso" + "windy-variant": "Prevalentemente ventoso" } } } \ No newline at end of file diff --git a/homeassistant/components/weather/translations/ko.json b/homeassistant/components/weather/translations/ko.json index 7aae9117e68..6828daca9f2 100644 --- a/homeassistant/components/weather/translations/ko.json +++ b/homeassistant/components/weather/translations/ko.json @@ -3,7 +3,7 @@ "_": { "clear-night": "\ub9d1\uc74c (\ubc24)", "cloudy": "\ud750\ub9bc", - "exceptional": "\uc608\uc678\uc0ac\ud56d", + "exceptional": "\uc774\ub840\uc801\uc778", "fog": "\uc548\uac1c", "hail": "\uc6b0\ubc15", "lightning": "\ubc88\uac1c", diff --git a/homeassistant/components/weather/translations/sl.json b/homeassistant/components/weather/translations/sl.json index e7ff67bac3c..e39c851894e 100644 --- a/homeassistant/components/weather/translations/sl.json +++ b/homeassistant/components/weather/translations/sl.json @@ -1,18 +1,18 @@ { "state": { "_": { - "clear-night": "Jasna, no\u010d", + "clear-night": "Jasno, no\u010d", "cloudy": "Obla\u010dno", "exceptional": "Izjemno", "fog": "Megla", "hail": "To\u010da", - "lightning": "Grmenje", + "lightning": "Strele", "lightning-rainy": "Grmenje, de\u017eevno", "partlycloudy": "Delno obla\u010dno", - "pouring": "Mo\u010dan de\u017e", + "pouring": "Lije", "rainy": "De\u017eevno", - "snowy": "Sne\u017eno", - "snowy-rainy": "Sne\u017eno, de\u017eevno", + "snowy": "Sne\u017eeno", + "snowy-rainy": "Sne\u017eeno, de\u017eevno", "sunny": "Son\u010dno", "windy": "Vetrovno", "windy-variant": "Vetrovno" diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 84ebdaddad0..47358d5008e 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -6,9 +6,11 @@ from aiohttp.web import Request, Response import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import HTTP_OK from homeassistant.core import callback +from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -54,7 +56,10 @@ def async_generate_id(): @bind_hass def async_generate_url(hass, webhook_id): """Generate the full URL for a webhook_id.""" - return "{}{}".format(hass.config.api.base_url, async_generate_path(webhook_id)) + return "{}{}".format( + get_url(hass, prefer_external=True, allow_cloud=False), + async_generate_path(webhook_id), + ) @callback @@ -71,7 +76,14 @@ async def async_handle_webhook(hass, webhook_id, request): # Always respond successfully to not give away if a hook exists or not. if webhook is None: - _LOGGER.warning("Received message for unregistered webhook %s", webhook_id) + peer_ip = request[KEY_REAL_IP] + _LOGGER.warning( + "Received message for unregistered webhook %s from %s", webhook_id, peer_ip + ) + # Look at content to provide some context for received webhook + # Limit to 64 chars to avoid flooding the log + content = await request.content.read(64) + _LOGGER.debug("%s...", content) return Response(status=HTTP_OK) try: diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index a441a70888b..0790ece9333 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -28,7 +28,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from .const import ATTR_SOUND_OUTPUT @@ -139,30 +138,15 @@ async def async_setup_tv_finalize(hass, config, conf, client): client.clear_state_update_callbacks() await client.disconnect() - async def async_load_platforms(_): - """Load platforms and event listener.""" - await async_connect(client) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) - if client.connection is None: - async_call_later(hass, 60, async_load_platforms) - _LOGGER.debug( - "No connection could be made with host %s, retrying in 60 seconds", - conf.get(CONF_HOST), - ) - return - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) - - hass.async_create_task( - hass.helpers.discovery.async_load_platform( - "media_player", DOMAIN, conf, config - ) - ) - hass.async_create_task( - hass.helpers.discovery.async_load_platform("notify", DOMAIN, conf, config) - ) - - await async_load_platforms(None) + await async_connect(client) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("media_player", DOMAIN, conf, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("notify", DOMAIN, conf, config) + ) async def async_request_configuration(hass, config, conf, client): diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index a19f42d7d56..3e443929012 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -8,7 +8,7 @@ from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerDevice +from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -109,14 +109,14 @@ def cmd(func): return wrapper -class LgWebOSMediaPlayerEntity(MediaPlayerDevice): +class LgWebOSMediaPlayerEntity(MediaPlayerEntity): """Representation of a LG webOS Smart TV.""" def __init__(self, client: WebOsClient, name: str, customize, on_script=None): """Initialize the webos device.""" self._client = client self._name = name - self._unique_id = client.software_info["device_id"] + self._unique_id = client.client_key self._customize = customize self._on_script = on_script diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 0ca1f950448..805a730e3d9 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -4,7 +4,7 @@ import logging import async_timeout -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as WEMO_DOMAIN @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoBinarySensor(BinarySensorDevice): +class WemoBinarySensor(BinarySensorEntity): """Representation a WeMo binary sensor.""" def __init__(self, device): diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index b8c05ead076..02b5e79d1c6 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util @@ -76,7 +76,7 @@ def setup_bridge(hass, bridge, async_add_entities): update_lights() -class WemoLight(Light): +class WemoLight(LightEntity): """Representation of a WeMo light.""" def __init__(self, device, update_lights): @@ -209,7 +209,7 @@ class WemoLight(Light): await self.hass.async_add_executor_job(self._update, force_update) -class WemoDimmer(Light): +class WemoDimmer(LightEntity): """Representation of a WeMo dimmer.""" def __init__(self, device): diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 4b6d99da200..203c495e974 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -5,7 +5,7 @@ import logging import async_timeout -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import convert @@ -47,7 +47,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoSwitch(SwitchDevice): +class WemoSwitch(SwitchEntity): """Representation of a WeMo switch.""" def __init__(self, device): diff --git a/homeassistant/components/wemo/translations/no.json b/homeassistant/components/wemo/translations/no.json index 1aecf403a4c..13476f63ec2 100644 --- a/homeassistant/components/wemo/translations/no.json +++ b/homeassistant/components/wemo/translations/no.json @@ -7,7 +7,7 @@ "step": { "confirm": { "description": "\u00d8nsker du \u00e5 sette opp Wemo?", - "title": "Wemo" + "title": "" } } } diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py new file mode 100644 index 00000000000..05d0d381e18 --- /dev/null +++ b/homeassistant/components/wiffi/__init__.py @@ -0,0 +1,230 @@ +"""Component for wiffi support.""" +import asyncio +from datetime import timedelta +import errno +import logging + +from wiffi import WiffiTcpServer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import utcnow + +from .const import ( + CHECK_ENTITIES_SIGNAL, + CREATE_ENTITY_SIGNAL, + DOMAIN, + UPDATE_ENTITY_SIGNAL, +) + +_LOGGER = logging.getLogger(__name__) + + +PLATFORMS = ["sensor", "binary_sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the wiffi component. config contains data from configuration.yaml.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up wiffi from a config entry, config_entry contains data from config entry database.""" + # create api object + api = WiffiIntegrationApi(hass) + api.async_setup(config_entry) + + # store api object + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = api + + try: + await api.server.start_server() + except OSError as exc: + if exc.errno != errno.EADDRINUSE: + _LOGGER.error("Start_server failed, errno: %d", exc.errno) + return False + _LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT]) + raise ConfigEntryNotReady from exc + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + api: "WiffiIntegrationApi" = hass.data[DOMAIN][config_entry.entry_id] + await api.server.close_server() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + api = hass.data[DOMAIN].pop(config_entry.entry_id) + api.shutdown() + + return unload_ok + + +def generate_unique_id(device, metric): + """Generate a unique string for the entity.""" + return f"{device.mac_address.replace(':', '')}-{metric.name}" + + +class WiffiIntegrationApi: + """API object for wiffi handling. Stored in hass.data.""" + + def __init__(self, hass): + """Initialize the instance.""" + self._hass = hass + self._server = None + self._known_devices = {} + self._periodic_callback = None + + def async_setup(self, config_entry): + """Set up api instance.""" + self._server = WiffiTcpServer(config_entry.data[CONF_PORT], self) + self._periodic_callback = async_track_time_interval( + self._hass, self._periodic_tick, timedelta(seconds=10) + ) + + def shutdown(self): + """Shutdown wiffi api. + + Remove listener for periodic callbacks. + """ + remove_listener = self._periodic_callback + if remove_listener is not None: + remove_listener() + + async def __call__(self, device, metrics): + """Process callback from TCP server if new data arrives from a device.""" + if device.mac_address not in self._known_devices: + # add empty set for new device + self._known_devices[device.mac_address] = set() + + for metric in metrics: + if metric.id not in self._known_devices[device.mac_address]: + self._known_devices[device.mac_address].add(metric.id) + async_dispatcher_send(self._hass, CREATE_ENTITY_SIGNAL, device, metric) + else: + async_dispatcher_send( + self._hass, + f"{UPDATE_ENTITY_SIGNAL}-{generate_unique_id(device, metric)}", + device, + metric, + ) + + @property + def server(self): + """Return TCP server instance for start + close.""" + return self._server + + @callback + def _periodic_tick(self, now=None): + """Check if any entity has timed out because it has not been updated.""" + async_dispatcher_send(self._hass, CHECK_ENTITIES_SIGNAL) + + +class WiffiEntity(Entity): + """Common functionality for all wiffi entities.""" + + def __init__(self, device, metric): + """Initialize the base elements of a wiffi entity.""" + self._id = generate_unique_id(device, metric) + self._device_info = { + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, device.mac_address) + }, + "identifiers": {(DOMAIN, device.mac_address)}, + "manufacturer": "stall.biz", + "name": f"{device.moduletype} {device.mac_address}", + "model": device.moduletype, + "sw_version": device.sw_version, + } + self._name = metric.description + self._expiration_date = None + self._value = None + + async def async_added_to_hass(self): + """Entity has been added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{UPDATE_ENTITY_SIGNAL}-{self._id}", + self._update_value_callback, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, CHECK_ENTITIES_SIGNAL, self._check_expiration_date + ) + ) + + @property + def should_poll(self): + """Disable polling because data driven .""" + return False + + @property + def device_info(self): + """Return wiffi device info which is shared between all entities of a device.""" + return self._device_info + + @property + def unique_id(self): + """Return unique id for entity.""" + return self._id + + @property + def name(self): + """Return entity name.""" + return self._name + + @property + def available(self): + """Return true if value is valid.""" + return self._value is not None + + def reset_expiration_date(self): + """Reset value expiration date. + + Will be called by derived classes after a value update has been received. + """ + self._expiration_date = utcnow() + timedelta(minutes=3) + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity.""" + + @callback + def _check_expiration_date(self): + """Periodically check if entity value has been updated. + + If there are no more updates from the wiffi device, the value will be + set to unavailable. + """ + if ( + self._value is not None + and self._expiration_date is not None + and utcnow() > self._expiration_date + ): + self._value = None + self.async_write_ha_state() diff --git a/homeassistant/components/wiffi/binary_sensor.py b/homeassistant/components/wiffi/binary_sensor.py new file mode 100644 index 00000000000..009fc2b4a67 --- /dev/null +++ b/homeassistant/components/wiffi/binary_sensor.py @@ -0,0 +1,53 @@ +"""Binary sensor platform support for wiffi devices.""" + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import WiffiEntity +from .const import CREATE_ENTITY_SIGNAL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up platform for a new integration. + + Called by the HA framework after async_forward_entry_setup has been called + during initialization of a new integration (= wiffi). + """ + + @callback + def _create_entity(device, metric): + """Create platform specific entities.""" + entities = [] + + if metric.is_bool: + entities.append(BoolEntity(device, metric)) + + async_add_entities(entities) + + async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity) + + +class BoolEntity(WiffiEntity, BinarySensorEntity): + """Entity for wiffi metrics which have a boolean value.""" + + def __init__(self, device, metric): + """Initialize the entity.""" + super().__init__(device, metric) + self._value = metric.value + self.reset_expiration_date() + + @property + def is_on(self): + """Return the state of the entity.""" + return self._value + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity. + + Called if a new message has been received from the wiffi device. + """ + self.reset_expiration_date() + self._value = metric.value + self.async_write_ha_state() diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py new file mode 100644 index 00000000000..82dbbb040ef --- /dev/null +++ b/homeassistant/components/wiffi/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for wiffi component. + +Used by UI to setup a wiffi integration. +""" +import errno + +import voluptuous as vol +from wiffi import WiffiTcpServer + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT +from homeassistant.core import callback + +from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import + + +class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Wiffi server setup config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow. + + Called after wiffi integration has been selected in the 'add integration + UI'. The user_input is set to None in this case. We will open a config + flow form then. + This function is also called if the form has been submitted. user_input + contains a dict with the user entered values then. + """ + if user_input is None: + return self._async_show_form() + + # received input from form or configuration.yaml + + try: + # try to start server to check whether port is in use + server = WiffiTcpServer(user_input[CONF_PORT]) + await server.start_server() + await server.close_server() + return self.async_create_entry( + title=f"Port {user_input[CONF_PORT]}", data=user_input + ) + except OSError as exc: + if exc.errno == errno.EADDRINUSE: + return self.async_abort(reason="addr_in_use") + return self.async_abort(reason="start_server_failed") + + @callback + def _async_show_form(self, errors=None): + """Show the config flow form to the user.""" + data_schema = {vol.Required(CONF_PORT, default=DEFAULT_PORT): int} + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {} + ) diff --git a/homeassistant/components/wiffi/const.py b/homeassistant/components/wiffi/const.py new file mode 100644 index 00000000000..6b71c89002f --- /dev/null +++ b/homeassistant/components/wiffi/const.py @@ -0,0 +1,12 @@ +"""Constants for the wiffi component.""" + +# Component domain, used to store component data in hass data. +DOMAIN = "wiffi" + +# Default port for TCP server +DEFAULT_PORT = 8189 + +# Signal name to send create/update to platform (sensor/binary_sensor) +CREATE_ENTITY_SIGNAL = "wiffi_create_entity_signal" +UPDATE_ENTITY_SIGNAL = "wiffi_update_entity_signal" +CHECK_ENTITIES_SIGNAL = "wiffi_check_entities_signal" diff --git a/homeassistant/components/wiffi/manifest.json b/homeassistant/components/wiffi/manifest.json new file mode 100644 index 00000000000..5be1286ad6f --- /dev/null +++ b/homeassistant/components/wiffi/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "wiffi", + "name": "Wiffi", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wiffi", + "requirements": ["wiffi==1.0.0"], + "dependencies": [], + "codeowners": [ + "@mampfes" + ] +} diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py new file mode 100644 index 00000000000..cc6befaf067 --- /dev/null +++ b/homeassistant/components/wiffi/sensor.py @@ -0,0 +1,125 @@ +"""Sensor platform support for wiffi devices.""" + +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, +) +from homeassistant.const import DEGREE, PRESSURE_MBAR, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import WiffiEntity +from .const import CREATE_ENTITY_SIGNAL +from .wiffi_strings import ( + WIFFI_UOM_DEGREE, + WIFFI_UOM_LUX, + WIFFI_UOM_MILLI_BAR, + WIFFI_UOM_PERCENT, + WIFFI_UOM_TEMP_CELSIUS, +) + +# map to determine HA device class from wiffi's unit of measurement +UOM_TO_DEVICE_CLASS_MAP = { + WIFFI_UOM_TEMP_CELSIUS: DEVICE_CLASS_TEMPERATURE, + WIFFI_UOM_PERCENT: DEVICE_CLASS_HUMIDITY, + WIFFI_UOM_MILLI_BAR: DEVICE_CLASS_PRESSURE, + WIFFI_UOM_LUX: DEVICE_CLASS_ILLUMINANCE, +} + +# map to convert wiffi unit of measurements to common HA uom's +UOM_MAP = { + WIFFI_UOM_DEGREE: DEGREE, + WIFFI_UOM_TEMP_CELSIUS: TEMP_CELSIUS, + WIFFI_UOM_MILLI_BAR: PRESSURE_MBAR, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up platform for a new integration. + + Called by the HA framework after async_forward_entry_setup has been called + during initialization of a new integration (= wiffi). + """ + + @callback + def _create_entity(device, metric): + """Create platform specific entities.""" + entities = [] + + if metric.is_number: + entities.append(NumberEntity(device, metric)) + elif metric.is_string: + entities.append(StringEntity(device, metric)) + + async_add_entities(entities) + + async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity) + + +class NumberEntity(WiffiEntity): + """Entity for wiffi metrics which have a number value.""" + + def __init__(self, device, metric): + """Initialize the entity.""" + super().__init__(device, metric) + self._device_class = UOM_TO_DEVICE_CLASS_MAP.get(metric.unit_of_measurement) + self._unit_of_measurement = UOM_MAP.get( + metric.unit_of_measurement, metric.unit_of_measurement + ) + self._value = metric.value + self.reset_expiration_date() + + @property + def device_class(self): + """Return the automatically determined device class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the value of the entity.""" + return self._value + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity. + + Called if a new message has been received from the wiffi device. + """ + self.reset_expiration_date() + self._unit_of_measurement = UOM_MAP.get( + metric.unit_of_measurement, metric.unit_of_measurement + ) + self._value = metric.value + self.async_write_ha_state() + + +class StringEntity(WiffiEntity): + """Entity for wiffi metrics which have a string value.""" + + def __init__(self, device, metric): + """Initialize the entity.""" + super().__init__(device, metric) + self._value = metric.value + self.reset_expiration_date() + + @property + def state(self): + """Return the value of the entity.""" + return self._value + + @callback + def _update_value_callback(self, device, metric): + """Update the value of the entity. + + Called if a new message has been received from the wiffi device. + """ + self.reset_expiration_date() + self._value = metric.value + self.async_write_ha_state() diff --git a/homeassistant/components/wiffi/strings.json b/homeassistant/components/wiffi/strings.json new file mode 100644 index 00000000000..36f836366a5 --- /dev/null +++ b/homeassistant/components/wiffi/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup TCP server for WIFFI devices", + "data": { + "port": "Server Port" + } + } + }, + "abort": { + "addr_in_use": "Server port already in use.", + "start_server_failed": "Start server failed." + } + } +} diff --git a/homeassistant/components/wiffi/translations/en.json b/homeassistant/components/wiffi/translations/en.json new file mode 100644 index 00000000000..bcaf0820bd5 --- /dev/null +++ b/homeassistant/components/wiffi/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addr_in_use": "Server port already in use.", + "start_server_failed": "Start server failed." + }, + "step": { + "user": { + "data": { + "port": "Server Port" + }, + "title": "Setup TCP server for WIFFI devices" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/no.json b/homeassistant/components/wiffi/translations/no.json new file mode 100644 index 00000000000..8e966a19c00 --- /dev/null +++ b/homeassistant/components/wiffi/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addr_in_use": "Serverport allerede i bruk.", + "start_server_failed": "Startserveren mislyktes." + }, + "step": { + "user": { + "data": { + "port": "Serverport" + }, + "title": "Sett opp TCP-server for WIFFI-enheter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/ru.json b/homeassistant/components/wiffi/translations/ru.json new file mode 100644 index 00000000000..e54389a0f34 --- /dev/null +++ b/homeassistant/components/wiffi/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addr_in_use": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "start_server_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440." + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 TCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 WIFFI" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/zh-Hant.json b/homeassistant/components/wiffi/translations/zh-Hant.json new file mode 100644 index 00000000000..0135fe86488 --- /dev/null +++ b/homeassistant/components/wiffi/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addr_in_use": "\u4f3a\u670d\u5668\u901a\u8a0a\u57e0\u5df2\u88ab\u4f7f\u7528\u3002", + "start_server_failed": "\u555f\u52d5\u4f3a\u670d\u5668\u5931\u6557\u3002" + }, + "step": { + "user": { + "data": { + "port": "\u4f3a\u670d\u5668\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a WIFFI \u8a2d\u5099 TCP \u4f3a\u670d\u5668" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/wiffi_strings.py b/homeassistant/components/wiffi/wiffi_strings.py new file mode 100644 index 00000000000..1bcd582feae --- /dev/null +++ b/homeassistant/components/wiffi/wiffi_strings.py @@ -0,0 +1,8 @@ +"""Definition of string used in wiffi json telegrams.""" + +# units of measurement +WIFFI_UOM_TEMP_CELSIUS = "gradC" +WIFFI_UOM_DEGREE = "grad" +WIFFI_UOM_PERCENT = "%" +WIFFI_UOM_MILLI_BAR = "mb" +WIFFI_UOM_LUX = "lux" diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 2d20183cb3d..1763a34fd87 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -29,6 +29,7 @@ from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.network import get_url from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) @@ -231,7 +232,7 @@ def _request_app_setup(hass, config): _configurator = hass.data[DOMAIN]["configuring"][DOMAIN] configurator.notify_errors(_configurator, error_msg) - start_url = f"{hass.config.api.base_url}{WINK_AUTH_CALLBACK_PATH}" + start_url = f"{get_url(hass)}{WINK_AUTH_CALLBACK_PATH}" description = f"""Please create a Wink developer app at https://developer.wink.com. @@ -269,7 +270,7 @@ def _request_oauth_completion(hass, config): """Call setup again.""" setup(hass, config) - start_url = f"{hass.config.api.base_url}{WINK_AUTH_START}" + start_url = f"{get_url(hass)}{WINK_AUTH_START}" description = f"Please authorize Wink by visiting {start_url}" @@ -349,7 +350,7 @@ def setup(hass, config): # Home . else: - redirect_uri = f"{hass.config.api.base_url}{WINK_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass)}{WINK_AUTH_CALLBACK_PATH}" wink_auth_start_url = pywink.get_authorization_url( config_file.get(ATTR_CLIENT_ID), redirect_uri diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 733022e91b1..89dc6a46815 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([WinkCameraDevice(camera, hass)]) -class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): +class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanelEntity): """Representation a Wink camera alarm.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index 6dd22a3f7b8..d8967dd064d 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -3,7 +3,7 @@ import logging import pywink -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DOMAIN, WinkDevice @@ -33,12 +33,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _id = sensor.object_id() + sensor.name() if _id not in hass.data[DOMAIN]["unique_ids"]: if sensor.capability() in SENSOR_TYPES: - add_entities([WinkBinarySensorDevice(sensor, hass)]) + add_entities([WinkBinarySensorEntity(sensor, hass)]) for key in pywink.get_keys(): _id = key.object_id() + key.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkBinarySensorDevice(key, hass)]) + add_entities([WinkBinarySensorEntity(key, hass)]) for sensor in pywink.get_smoke_and_co_detectors(): _id = sensor.object_id() + sensor.name() @@ -68,19 +68,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for door_bell_sensor in pywink.get_door_bells(): _id = door_bell_sensor.object_id() + door_bell_sensor.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkBinarySensorDevice(door_bell_sensor, hass)]) + add_entities([WinkBinarySensorEntity(door_bell_sensor, hass)]) for camera_sensor in pywink.get_cameras(): _id = camera_sensor.object_id() + camera_sensor.name() if _id not in hass.data[DOMAIN]["unique_ids"]: try: if camera_sensor.capability() in SENSOR_TYPES: - add_entities([WinkBinarySensorDevice(camera_sensor, hass)]) + add_entities([WinkBinarySensorEntity(camera_sensor, hass)]) except AttributeError: _LOGGER.info("Device isn't a sensor, skipping") -class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): +class WinkBinarySensorEntity(WinkDevice, BinarySensorEntity): """Representation of a Wink binary sensor.""" def __init__(self, wink, hass): @@ -115,7 +115,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): return super().device_state_attributes -class WinkSmokeDetector(WinkBinarySensorDevice): +class WinkSmokeDetector(WinkBinarySensorEntity): """Representation of a Wink Smoke detector.""" @property @@ -126,7 +126,7 @@ class WinkSmokeDetector(WinkBinarySensorDevice): return _attributes -class WinkHub(WinkBinarySensorDevice): +class WinkHub(WinkBinarySensorEntity): """Representation of a Wink Hub.""" @property @@ -146,7 +146,7 @@ class WinkHub(WinkBinarySensorDevice): return _attributes -class WinkRemote(WinkBinarySensorDevice): +class WinkRemote(WinkBinarySensorEntity): """Representation of a Wink Lutron Connected bulb remote.""" @property @@ -165,7 +165,7 @@ class WinkRemote(WinkBinarySensorDevice): return None -class WinkButton(WinkBinarySensorDevice): +class WinkButton(WinkBinarySensorEntity): """Representation of a Wink Relay button.""" @property @@ -177,7 +177,7 @@ class WinkButton(WinkBinarySensorDevice): return _attributes -class WinkGang(WinkBinarySensorDevice): +class WinkGang(WinkBinarySensorEntity): """Representation of a Wink Relay gang.""" @property diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 85d477313f1..28be557c65e 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -3,7 +3,7 @@ import logging import pywink -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([WinkAC(climate, hass)]) -class WinkThermostat(WinkDevice, ClimateDevice): +class WinkThermostat(WinkDevice, ClimateEntity): """Representation of a Wink thermostat.""" @property @@ -381,7 +381,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): return return_value -class WinkAC(WinkDevice, ClimateDevice): +class WinkAC(WinkDevice, ClimateEntity): """Representation of a Wink air conditioner.""" @property diff --git a/homeassistant/components/wink/cover.py b/homeassistant/components/wink/cover.py index 1ce7f9b8875..f2f4241c64d 100644 --- a/homeassistant/components/wink/cover.py +++ b/homeassistant/components/wink/cover.py @@ -1,7 +1,7 @@ """Support for Wink covers.""" import pywink -from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.cover import ATTR_POSITION, CoverEntity from . import DOMAIN, WinkDevice @@ -12,18 +12,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for shade in pywink.get_shades(): _id = shade.object_id() + shade.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCoverDevice(shade, hass)]) + add_entities([WinkCoverEntity(shade, hass)]) for shade in pywink.get_shade_groups(): _id = shade.object_id() + shade.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCoverDevice(shade, hass)]) + add_entities([WinkCoverEntity(shade, hass)]) for door in pywink.get_garage_doors(): _id = door.object_id() + door.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCoverDevice(door, hass)]) + add_entities([WinkCoverEntity(door, hass)]) -class WinkCoverDevice(WinkDevice, CoverDevice): +class WinkCoverEntity(WinkDevice, CoverEntity): """Representation of a Wink cover device.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/wink/light.py b/homeassistant/components/wink/light.py index bd125e6a7c2..4d20cf4dd5a 100644 --- a/homeassistant/components/wink/light.py +++ b/homeassistant/components/wink/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) from homeassistant.util import color as color_util from homeassistant.util.color import ( @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([WinkLight(light, hass)]) -class WinkLight(WinkDevice, Light): +class WinkLight(WinkDevice, LightEntity): """Representation of a Wink light.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 57cf9d304ec..221d6d8165e 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -4,7 +4,7 @@ import logging import pywink import voluptuous as vol -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -133,7 +133,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class WinkLockDevice(WinkDevice, LockDevice): +class WinkLockDevice(WinkDevice, LockEntity): """Representation of a Wink lock.""" async def async_added_to_hass(self): diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py index dae6acf91bf..0ce31762c7a 100644 --- a/homeassistant/components/wink/water_heater.py +++ b/homeassistant/components/wink/water_heater.py @@ -14,7 +14,7 @@ from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([WinkWaterHeater(water_heater, hass)]) -class WinkWaterHeater(WinkDevice, WaterHeaterDevice): +class WinkWaterHeater(WinkDevice, WaterHeaterEntity): """Representation of a Wink water heater.""" @property diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index eae8c17edcd..b92cdeb0bca 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS, STATE_OFF, STATE_ON from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -88,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.add_job(platform.install_push_notifications, sensors) -class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): +class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorEntity): """A binary sensor implementation for WirelessTags.""" def __init__(self, api, tag, sensor_type): diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 79242394f7f..9027f92aec0 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -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_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches, True) -class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice): +class WirelessTagSwitch(WirelessTagBaseSensor, SwitchEntity): """A switch implementation for Wireless Sensor Tags.""" def __init__(self, api, tag, switch_type): diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index 3b2ee2eff9b..e98426446fc 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -15,7 +15,7 @@ "data": { "profile": "Perfil" }, - "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s\u2019etiquetaran correctament.", + "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s'etiquetaran correctament.", "title": "Perfil d'usuari." } } diff --git a/homeassistant/components/withings/translations/es-419.json b/homeassistant/components/withings/translations/es-419.json index 4fc2dec0ac2..d06470b213f 100644 --- a/homeassistant/components/withings/translations/es-419.json +++ b/homeassistant/components/withings/translations/es-419.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." + }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + }, + "profile": { + "data": { + "profile": "Perfil" + }, + "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", + "title": "Perfil del usuario." + } } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 19376d363cb..d162c06e761 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -5,7 +5,7 @@ "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." }, "create_entry": { - "default": "Autenticado correctamente con Withings para el perfil seleccionado." + "default": "Autenticado correctamente con Withings." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index f15cfc1a2de..da1d3440611 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, "profile": { "data": { diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 0922f1b3344..44c341ca2ca 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Withings-integreringen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", + "missing_configuration": "Withings-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, "create_entry": { - "default": "Vellykket godkjent med Withings." + "default": "Vellykket godkjenning med Withings." }, "step": { "pick_implementation": { - "title": "Velg autentiseringsmetode" + "title": "Velg godkjenningsmetode" }, "profile": { "data": { diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index a45141ff50b..1b59e61a768 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "Integracja z Withings nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { @@ -9,7 +9,7 @@ }, "step": { "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelnienia" + "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" }, "profile": { "data": { diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index f99071a2615..5a945c14a84 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -9,7 +9,7 @@ }, "step": { "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, "profile": { "data": { diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index beda19b8101..6b86be6265b 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -17,7 +17,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers import config_validation as cv, entity_platform @@ -78,7 +78,7 @@ async def async_setup_entry( async_add_entities(lights, True) -class WLEDLight(Light, WLEDDeviceEntity): +class WLEDLight(LightEntity, WLEDDeviceEntity): """Defines a WLED light.""" def __init__( diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json index 4200fd96301..1197517917c 100644 --- a/homeassistant/components/wled/strings.json +++ b/homeassistant/components/wled/strings.json @@ -4,17 +4,21 @@ "step": { "user": { "description": "Set up your WLED to integrate with Home Assistant.", - "data": { "host": "Host or IP address" } + "data": { + "host": "[%key:common::config_flow::data::host%]" + } }, "zeroconf_confirm": { "description": "Do you want to add the WLED named `{name}` to Home Assistant?", "title": "Discovered WLED device" } }, - "error": { "connection_error": "Failed to connect to WLED device." }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, "abort": { "already_configured": "This WLED device is already configured.", "connection_error": "Failed to connect to WLED device." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 85a1f261d94..440819927b1 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -2,7 +2,7 @@ import logging from typing import Any, Callable, Dict, List, Optional -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -37,7 +37,7 @@ async def async_setup_entry( async_add_entities(switches, True) -class WLEDSwitch(WLEDDeviceEntity, SwitchDevice): +class WLEDSwitch(WLEDDeviceEntity, SwitchEntity): """Defines a WLED switch.""" def __init__( diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json index 7c63779d5ac..362798be5bd 100644 --- a/homeassistant/components/wled/translations/en.json +++ b/homeassistant/components/wled/translations/en.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Host or IP address" + "host": "Host" }, "description": "Set up your WLED to integrate with Home Assistant.", "title": "Link your WLED" diff --git a/homeassistant/components/wled/translations/es-419.json b/homeassistant/components/wled/translations/es-419.json index a9107341e37..b5973638373 100644 --- a/homeassistant/components/wled/translations/es-419.json +++ b/homeassistant/components/wled/translations/es-419.json @@ -10,6 +10,9 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index b5973638373..6b2c46f25e0 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", + "connection_error": "No se ha podido conectar al dispositivo WLED." }, "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "connection_error": "No se ha podido conectar al dispositivo WLED." }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", + "flow_title": "WLED: {name}", "step": { "user": { "data": { "host": "Host o direcci\u00f3n IP" }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Configura el WLED para integrarlo con Home Assistant.", + "title": "Vincula tu WLED" }, "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "\u00bfQuieres a\u00f1adir el WLED `{name}` a Home Assistant?", + "title": "Dispositivo WLED detectado" } } } diff --git a/homeassistant/components/wled/translations/ko.json b/homeassistant/components/wled/translations/ko.json index b811023e88e..12d964f25c6 100644 --- a/homeassistant/components/wled/translations/ko.json +++ b/homeassistant/components/wled/translations/ko.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "already_configured": "\uc774 WLED \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", + "flow_title": "WLED: {name}", "step": { "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + "host": "\ud638\uc2a4\ud2b8" }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "WLED \uc5f0\uacb0\ud558\uae30" }, "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c WLED \uae30\uae30" } } } diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json index 34c645e9802..fd3b5f94cbe 100644 --- a/homeassistant/components/wled/translations/no.json +++ b/homeassistant/components/wled/translations/no.json @@ -13,7 +13,7 @@ "data": { "host": "Vert eller IP-adresse" }, - "description": "Konfigurer WLED til \u00e5 integreres med Home Assistant.", + "description": "Sett opp WLED til \u00e5 integreres med Home Assistant.", "title": "Linken din WLED" }, "zeroconf_confirm": { diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index 8e4d8cec492..6db76e82a0a 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" }, "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", + "flow_title": "WLED: {name}", "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP" + "host": "[%key_id:common::config_flow::data::host%]" }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Konfiguracja WLED w celu integracji z Home Assistant'em.", + "title": "Po\u0142\u0105cz z WLED" }, "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistant'a?", + "title": "Wykryto urz\u0105dzenie WLED" } } } diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index 21b5282eb6d..867b2ca2ab6 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -11,9 +11,9 @@ "step": { "user": { "data": { - "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 WLED \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 WLED.", "title": "WLED" }, "zeroconf_confirm": { diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 1b3b3f9fb2a..c712a3682ad 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", "title": "\u9023\u7d50 WLED" diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 0aa1f5bfc42..1613f10d66a 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -6,7 +6,7 @@ from typing import Any import holidays import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME, WEEKDAYS import homeassistant.helpers.config_validation as cv @@ -119,7 +119,7 @@ def get_date(date): return date -class IsWorkdaySensor(BinarySensorDevice): +class IsWorkdaySensor(BinarySensorEntity): """Implementation of a Workday sensor.""" def __init__(self, obj_holidays, workdays, excludes, days_offset, name): diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index 4d9ff6e2235..954088e4b21 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -85,7 +85,7 @@ class Wunderlist: def _list_by_name(self, name): """Return a list ID by name.""" lists = self._client.get_lists() - tmp = [l for l in lists if l["title"] == name] + tmp = [lst for lst in lists if lst["title"] == name] if tmp: return tmp[0]["id"] return None diff --git a/homeassistant/components/wwlln/translations/es-419.json b/homeassistant/components/wwlln/translations/es-419.json index 1732a5b43bc..11ae8c64359 100644 --- a/homeassistant/components/wwlln/translations/es-419.json +++ b/homeassistant/components/wwlln/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/translations/fi.json b/homeassistant/components/wwlln/translations/fi.json new file mode 100644 index 00000000000..1b1b454585f --- /dev/null +++ b/homeassistant/components/wwlln/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Leveysaste", + "longitude": "Pituusaste" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 131cc61ed61..3943c081eef 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - Light, + LightEntity, ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -50,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(X10Light(light, is_cm11a) for light in config[CONF_DEVICES]) -class X10Light(Light): +class X10Light(LightEntity): """Representation of an X10 Light.""" def __init__(self, light, is_cm11a): diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 2c95198f348..01caddb7eb5 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Xiaomi aqara binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -99,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice): +class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): """Representation of a base XiaomiBinarySensor.""" def __init__(self, device, name, xiaomi_hub, data_key, device_class): diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index da6b24d616a..52d2487e74f 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -1,7 +1,7 @@ """Support for Xiaomi curtain.""" import logging -from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.cover import ATTR_POSITION, CoverEntity from . import PY_XIAOMI_GATEWAY, XiaomiDevice @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class XiaomiGenericCover(XiaomiDevice, CoverDevice): +class XiaomiGenericCover(XiaomiDevice, CoverEntity): """Representation of a XiaomiGenericCover.""" def __init__(self, device, name, data_key, xiaomi_hub): diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 5096bc25d24..f1cd17f9dee 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) import homeassistant.util.color as color_util @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class XiaomiGatewayLight(XiaomiDevice, Light): +class XiaomiGatewayLight(XiaomiDevice, LightEntity): """Representation of a XiaomiGatewayLight.""" def __init__(self, device, name, xiaomi_hub): diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index ed71e05bf5f..c3835f83391 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -1,7 +1,7 @@ """Support for Xiaomi Aqara locks.""" import logging -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockEntity from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -32,7 +32,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class XiaomiAqaraLock(LockDevice, XiaomiDevice): +class XiaomiAqaraLock(LockEntity, XiaomiDevice): """Representation of a XiaomiAqaraLock.""" def __init__(self, device, name, xiaomi_hub): diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 90201e9c452..e711eab46fb 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -1,7 +1,7 @@ """Support for Xiaomi Aqara binary sensors.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import PY_XIAOMI_GATEWAY, XiaomiDevice @@ -79,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): +class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): """Representation of a XiaomiPlug.""" def __init__(self, device, name, data_key, supports_power_consumption, xiaomi_hub): diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9abc871b9b4..0dd03e42e7d 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -1 +1,69 @@ """Support for Xiaomi Miio.""" +import logging + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.helpers import device_registry as dr + +from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY +from .const import DOMAIN +from .gateway import ConnectXiaomiGateway + +_LOGGER = logging.getLogger(__name__) + +GATEWAY_PLATFORMS = ["alarm_control_panel"] + + +async def async_setup(hass: core.HomeAssistant, config: dict): + """Set up the Xiaomi Miio component.""" + return True + + +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up the Xiaomi Miio components from a config entry.""" + hass.data[DOMAIN] = {} + if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: + if not await async_setup_gateway_entry(hass, entry): + return False + + return True + + +async def async_setup_gateway_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up the Xiaomi Gateway component from a config entry.""" + host = entry.data[CONF_HOST] + token = entry.data[CONF_TOKEN] + name = entry.title + gateway_id = entry.data["gateway_id"] + + # Connect to gateway + gateway = ConnectXiaomiGateway(hass) + if not await gateway.async_connect_gateway(host, token): + return False + gateway_info = gateway.gateway_info + + hass.data[DOMAIN][entry.entry_id] = gateway.gateway_device + + gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}" + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)}, + identifiers={(DOMAIN, gateway_id)}, + manufacturer="Xiaomi", + name=name, + model=gateway_model, + sw_version=gateway_info.firmware_version, + ) + + for component in GATEWAY_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py new file mode 100644 index 00000000000..dccd94dc963 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -0,0 +1,150 @@ +"""Support for Xiomi Gateway alarm control panels.""" + +from functools import partial +import logging + +from miio import DeviceException + +from homeassistant.components.alarm_control_panel import ( + SUPPORT_ALARM_ARM_AWAY, + AlarmControlPanelEntity, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, +) + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +XIAOMI_STATE_ARMED_VALUE = "on" +XIAOMI_STATE_DISARMED_VALUE = "off" +XIAOMI_STATE_ARMING_VALUE = "oning" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi Gateway Alarm from a config entry.""" + entities = [] + gateway = hass.data[DOMAIN][config_entry.entry_id] + entity = XiaomiGatewayAlarm( + gateway, + f"{config_entry.title} Alarm", + config_entry.data["model"], + config_entry.data["mac"], + config_entry.data["gateway_id"], + ) + entities.append(entity) + async_add_entities(entities) + + +class XiaomiGatewayAlarm(AlarmControlPanelEntity): + """Representation of the XiaomiGatewayAlarm.""" + + def __init__( + self, gateway_device, gateway_name, model, mac_address, gateway_device_id + ): + """Initialize the entity.""" + self._gateway = gateway_device + self._name = gateway_name + self._gateway_device_id = gateway_device_id + self._unique_id = f"{model}-{mac_address}" + self._icon = "mdi:shield-home" + self._available = None + self._state = None + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def device_id(self): + """Return the device id of the gateway.""" + return self._gateway_device_id + + @property + def device_info(self): + """Return the device info of the gateway.""" + return { + "identifiers": {(DOMAIN, self._gateway_device_id)}, + } + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a device command handling error messages.""" + try: + result = await self.hass.async_add_executor_job( + partial(func, *args, **kwargs) + ) + _LOGGER.debug("Response received from miio device: %s", result) + except DeviceException as exc: + _LOGGER.error(mask_error, exc) + + async def async_alarm_arm_away(self, code=None): + """Turn on.""" + await self._try_command( + "Turning the alarm on failed: %s", self._gateway.alarm.on + ) + + async def async_alarm_disarm(self, code=None): + """Turn off.""" + await self._try_command( + "Turning the alarm off failed: %s", self._gateway.alarm.off + ) + + async def async_update(self): + """Fetch state from the device.""" + try: + state = await self.hass.async_add_executor_job(self._gateway.alarm.status) + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + return + + _LOGGER.debug("Got new state: %s", state) + + self._available = True + + if state == XIAOMI_STATE_ARMED_VALUE: + self._state = STATE_ALARM_ARMED_AWAY + elif state == XIAOMI_STATE_DISARMED_VALUE: + self._state = STATE_ALARM_DISARMED + elif state == XIAOMI_STATE_ARMING_VALUE: + self._state = STATE_ALARM_ARMING + else: + _LOGGER.warning( + "New state (%s) doesn't match expected values: %s/%s/%s", + state, + XIAOMI_STATE_ARMED_VALUE, + XIAOMI_STATE_DISARMED_VALUE, + XIAOMI_STATE_ARMING_VALUE, + ) + self._state = None + + _LOGGER.debug("State value: %s", self._state) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py new file mode 100644 index 00000000000..092f5d85d30 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow to configure Xiaomi Miio.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN + +# pylint: disable=unused-import +from .const import DOMAIN +from .gateway import ConnectXiaomiGateway + +_LOGGER = logging.getLogger(__name__) + +CONF_FLOW_TYPE = "config_flow_device" +CONF_GATEWAY = "gateway" +DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" + +GATEWAY_CONFIG = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str, + } +) + +CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool}) + + +class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Xiaomi Miio config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + # Check which device needs to be connected. + if user_input[CONF_GATEWAY]: + return await self.async_step_gateway() + + errors["base"] = "no_device_selected" + + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def async_step_gateway(self, user_input=None): + """Handle a flow initialized by the user to configure a gateway.""" + errors = {} + if user_input is not None: + host = user_input[CONF_HOST] + token = user_input[CONF_TOKEN] + + # Try to connect to a Xiaomi Gateway. + connect_gateway_class = ConnectXiaomiGateway(self.hass) + await connect_gateway_class.async_connect_gateway(host, token) + gateway_info = connect_gateway_class.gateway_info + + if gateway_info is not None: + unique_id = f"{gateway_info.model}-{gateway_info.mac_address}-gateway" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_FLOW_TYPE: CONF_GATEWAY, + CONF_HOST: host, + CONF_TOKEN: token, + "gateway_id": unique_id, + "model": gateway_info.model, + "mac": gateway_info.mac_address, + }, + ) + + errors["base"] = "connect_error" + + return self.async_show_form( + step_id="gateway", data_schema=GATEWAY_CONFIG, errors=errors + ) diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py new file mode 100644 index 00000000000..2195c9eecdc --- /dev/null +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -0,0 +1,47 @@ +"""Code to handle a Xiaomi Gateway.""" +import logging + +from miio import DeviceException, gateway + +_LOGGER = logging.getLogger(__name__) + + +class ConnectXiaomiGateway: + """Class to async connect to a Xiaomi Gateway.""" + + def __init__(self, hass): + """Initialize the entity.""" + self._hass = hass + self._gateway_device = None + self._gateway_info = None + + @property + def gateway_device(self): + """Return the class containing all connections to the gateway.""" + return self._gateway_device + + @property + def gateway_info(self): + """Return the class containing gateway info.""" + return self._gateway_info + + async def async_connect_gateway(self, host, token): + """Connect to the Xiaomi Gateway.""" + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + try: + self._gateway_device = gateway.Gateway(host, token) + self._gateway_info = await self._hass.async_add_executor_job( + self._gateway_device.info + ) + except DeviceException: + _LOGGER.error( + "DeviceException during setup of xiaomi gateway with host %s", host + ) + return False + _LOGGER.debug( + "%s %s %s detected", + self._gateway_info.model, + self._gateway_info.firmware_version, + self._gateway_info.hardware_version, + ) + return True diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index c4ea831ceeb..cc9343aa2c0 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -24,7 +24,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light, + LightEntity, ) from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady @@ -236,7 +236,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class XiaomiPhilipsAbstractLight(Light): +class XiaomiPhilipsAbstractLight(LightEntity): """Representation of a Abstract Xiaomi Philips Light.""" def __init__(self, name, light, model, unique_id): diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 1db01321285..468389b4626 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -1,6 +1,7 @@ { "domain": "xiaomi_miio", - "name": "Xiaomi miio", + "name": "Xiaomi Miio", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": ["construct==2.9.45", "python-miio==0.5.0.1"], "codeowners": ["@rytilahti", "@syssi"] diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 8c4d68208b4..fb188368127 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -12,7 +12,7 @@ from homeassistant.components.remote import ( ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, PLATFORM_SCHEMA, - RemoteDevice, + RemoteEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -165,7 +165,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class XiaomiMiioRemote(RemoteDevice): +class XiaomiMiioRemote(RemoteEntity): """Representation of a Xiaomi Miio Remote device.""" def __init__(self, friendly_name, device, unique_id, slot, timeout, commands): diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json new file mode 100644 index 00000000000..150cebe084a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "title": "Xiaomi Miio", + "description": "Select to which device you want to connect.", + "data": { + "gateway": "Connect to a Xiaomi Gateway" + } + }, + "gateway": { + "title": "Connect to a Xiaomi Gateway", + "description": "You will need the API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions.", + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "token": "API Token", + "name": "Name of the Gateway" + } + } + }, + "error": { + "connect_error": "[%key:common::config_flow::error::cannot_connect%]", + "no_device_selected": "No device selected, please select one device." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index c66f9b745f5..d8552244ce8 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -13,7 +13,7 @@ from miio import ( # pylint: disable=import-error from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -217,7 +217,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class XiaomiPlugGenericSwitch(SwitchDevice): +class XiaomiPlugGenericSwitch(SwitchEntity): """Representation of a Xiaomi Plug Generic.""" def __init__(self, name, plug, model, unique_id): diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json new file mode 100644 index 00000000000..1c6c21e02f0 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "[%key::common::config_flow::abort::already_configured_device%]" + }, + "error": { + "connect_error": "[%key::common::config_flow::error::cannot_connect%]", + "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un." + }, + "step": { + "gateway": { + "data": { + "host": "Adre\u00e7a IP", + "name": "Nom de la passarel\u00b7la", + "token": "Token de l'API" + }, + "description": "Necessitar\u00e0s el token de l'API, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi" + }, + "user": { + "data": { + "gateway": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi" + }, + "description": "Selecciona a quin dispositiu vols connectar-te.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json new file mode 100644 index 00000000000..9064eacf899 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "connect_error": "Nepoda\u0159ilo se p\u0159ipojit, zkuste to znovu", + "no_device_selected": "Nebylo vybr\u00e1no \u017e\u00e1dn\u00e9 za\u0159\u00edzen\u00ed, vyberte jedno za\u0159\u00edzen\u00ed." + }, + "step": { + "gateway": { + "data": { + "host": "IP adresa", + "name": "N\u00e1zev br\u00e1ny", + "token": "API Token" + }, + "description": "Je vy\u017eadov\u00e1n token API, pokyny naleznete na str\u00e1nce https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" + }, + "user": { + "data": { + "gateway": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" + }, + "description": "Vyberte, ke kter\u00e9mu za\u0159\u00edzen\u00ed se chcete p\u0159ipojit.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json new file mode 100644 index 00000000000..d60099d7538 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "connect_error": "Verbindung fehlgeschlagen", + "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus." + }, + "step": { + "gateway": { + "data": { + "host": "IP Adresse", + "name": "Name des Gateways", + "token": "API-Token" + }, + "description": "Sie ben\u00f6tigen das API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + }, + "user": { + "data": { + "gateway": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + }, + "description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json new file mode 100644 index 00000000000..f67df5b7826 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "connect_error": "Failed to connect", + "no_device_selected": "No device selected, please select one device." + }, + "step": { + "gateway": { + "data": { + "host": "IP address", + "name": "Name of the Gateway", + "token": "API Token" + }, + "description": "You will need the API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions.", + "title": "Connect to a Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Connect to a Xiaomi Gateway" + }, + "description": "Select to which device you want to connect.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es-419.json b/homeassistant/components/xiaomi_miio/translations/es-419.json new file mode 100644 index 00000000000..ac9e6077ce5 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "connect_error": "No se pudo conectar, intente nuevamente", + "no_device_selected": "Ning\u00fan dispositivo seleccionado, seleccione un dispositivo." + }, + "step": { + "gateway": { + "data": { + "host": "Direcci\u00f3n IP", + "name": "Nombre de la puerta de enlace", + "token": "Token API" + }, + "description": "Necesitar\u00e1 el token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones.", + "title": "Conectarse a una puerta de enlace Xiaomi" + }, + "user": { + "data": { + "gateway": "Conectarse a una puerta de enlace Xiaomi" + }, + "description": "Seleccione a qu\u00e9 dispositivo desea conectarse.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json new file mode 100644 index 00000000000..5760a12ed04 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "connect_error": "No se ha podido conectar", + "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo." + }, + "step": { + "gateway": { + "data": { + "host": "Direcci\u00f3n IP", + "name": "Nombre del Gateway", + "token": "Token API" + }, + "description": "Necesitar\u00e1s el Token API, consulta https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para instrucciones.", + "title": "Conectar con un Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Conectar con un Xiaomi Gateway" + }, + "description": "Selecciona a qu\u00e9 dispositivo quieres conectar.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/fi.json b/homeassistant/components/xiaomi_miio/translations/fi.json new file mode 100644 index 00000000000..669f5a34fb9 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/fi.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "connect_error": "Yhteyden muodostaminen ep\u00e4onnistui. Yrit\u00e4 uudelleen", + "no_device_selected": "Ei valittuja laitteita. Ole hyv\u00e4 ja valitse yksi." + }, + "step": { + "gateway": { + "data": { + "host": "IP-osoite", + "name": "Yhdysk\u00e4yt\u00e4v\u00e4n nimi", + "token": "API-tunnus" + }, + "title": "Yhdist\u00e4 Xiaomi Gatewayhin" + }, + "user": { + "data": { + "gateway": "Yhdist\u00e4 Xiaomi Gatewayhin" + }, + "description": "Valitse laite, johon haluat muodostaa yhteyden.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json new file mode 100644 index 00000000000..bf00d30bc6e --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "connect_error": "Impossible de se connecter, veuillez r\u00e9essayer", + "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil." + }, + "step": { + "gateway": { + "data": { + "host": "adresse IP", + "name": "Nom de la passerelle", + "token": "Jeton d'API" + }, + "description": "Vous aurez besoin du jeton API, voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions.", + "title": "Se connecter \u00e0 la passerelle Xiaomi" + }, + "user": { + "data": { + "gateway": "Se connecter \u00e0 la passerelle Xiaomi" + }, + "description": "S\u00e9lectionnez \u00e0 quel appareil vous souhaitez vous connecter.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json new file mode 100644 index 00000000000..9cdda6533b5 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "connect_error": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet." + }, + "step": { + "gateway": { + "data": { + "host": "IP-c\u00edm" + }, + "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token" + }, + "user": { + "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. " + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json new file mode 100644 index 00000000000..911e16a3dc5 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "connect_error": "Impossibile connettersi", + "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo." + }, + "step": { + "gateway": { + "data": { + "host": "Indirizzo IP", + "name": "Nome del Gateway", + "token": "Token API" + }, + "description": "Sar\u00e0 necessario il token API, consultare https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per istruzioni.", + "title": "Connessione a un Xiaomi Gateway " + }, + "user": { + "data": { + "gateway": "Connettiti a un Xiaomi Gateway" + }, + "description": "Selezionare a quale dispositivo si desidera collegare.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ko.json b/homeassistant/components/xiaomi_miio/translations/ko.json new file mode 100644 index 00000000000..dbf3e091c5e --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/ko.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connect_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + }, + "step": { + "gateway": { + "data": { + "host": "IP \uc8fc\uc18c", + "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984", + "token": "API \ud1a0\ud070" + }, + "description": "API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" + }, + "user": { + "data": { + "gateway": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0" + }, + "description": "\uc5f0\uacb0\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/lb.json b/homeassistant/components/xiaomi_miio/translations/lb.json new file mode 100644 index 00000000000..05c8e2354b8 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "connect_error": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "no_device_selected": "Keen Apparat ausgewielt, wiel een Apparat aus w.e.g." + }, + "step": { + "gateway": { + "data": { + "host": "IP Adresse", + "name": "Numm vum Gateway", + "token": "API Jeton" + }, + "description": "Du brauchs den API Jeton, kuck https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token fir Instruktiounen.", + "title": "Mat enger Xiaomi Gateway verbannen" + }, + "user": { + "data": { + "gateway": "Mat enger Xiaomi Gateway verbannen" + }, + "description": "Wielt den Apparat aus dee soll verbonne ginn", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json new file mode 100644 index 00000000000..cb4aa077ba0 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "connect_error": "Verbinding mislukt, probeer het opnieuw", + "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft" + }, + "step": { + "gateway": { + "data": { + "host": "IP-adres", + "name": "Naam van de gateway", + "token": "API-token" + }, + "description": "U heeft het API-token nodig, zie https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token voor instructies.", + "title": "Maak verbinding met een Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Maak verbinding met een Xiaomi Gateway" + }, + "description": "Selecteer het apparaat waarmee u verbinding wilt maken", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json new file mode 100644 index 00000000000..5a92830cdb7 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet." + }, + "step": { + "gateway": { + "data": { + "host": "IP adresse", + "name": "Navnet p\u00e5 gatewayen", + "token": "API-token" + }, + "description": "Du trenger API-tilgangstoken, se [https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token](https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token) for instruksjoner.", + "title": "Koble til en Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Koble til en Xiaomi Gateway" + }, + "description": "Velg hvilken enhet du vil koble til.", + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json new file mode 100644 index 00000000000..da144095ad4 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie." + }, + "step": { + "gateway": { + "data": { + "host": "Adres IP", + "name": "Nazwa bramki", + "token": "Klucz API" + }, + "description": "B\u0119dziesz potrzebowa\u0107 tokenu API, odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje.", + "title": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi" + }, + "user": { + "data": { + "gateway": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi" + }, + "description": "Wybierz urz\u0105dzenie, z kt\u00f3rym chcesz si\u0119 po\u0142\u0105czy\u0107.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json new file mode 100644 index 00000000000..fef7362537f --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -0,0 +1,29 @@ +{ + "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": { + "connect_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." + }, + "step": { + "gateway": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, + "user": { + "data": { + "gateway": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sl.json b/homeassistant/components/xiaomi_miio/translations/sl.json new file mode 100644 index 00000000000..d8fc0bf28f7 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "connect_error": "Povezava ni uspela", + "no_device_selected": "Izbrana ni nobena naprava, izberite eno napravo." + }, + "step": { + "gateway": { + "data": { + "host": "IP naslov", + "name": "Ime prehoda", + "token": "API \u017eeton" + }, + "description": "Potrebujete API \u017deton, glej https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token za navodila.", + "title": "Pove\u017eite se s prehodom Xiaomi" + }, + "user": { + "data": { + "gateway": "Pove\u017eite se s prehodom Xiaomi" + }, + "description": "Izberite, s katero napravo se \u017eelite povezati.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json new file mode 100644 index 00000000000..aee988f0a04 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "connect_error": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "no_device_selected": "Ingen enhet har valts, v\u00e4lj en enhet." + }, + "step": { + "gateway": { + "data": { + "host": "IP-adress", + "name": "Namnet p\u00e5 Gatewayen", + "token": "API Token" + }, + "description": "Du beh\u00f6ver en API token, se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token f\u00f6r mer instruktioner.", + "title": "Anslut till en Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Anslut till en Xiaomi Gateway" + }, + "description": "V\u00e4lj den enhet som du vill ansluta till." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json new file mode 100644 index 00000000000..9a7dfcb37cc --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "connect_error": "\u8fde\u63a5\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5", + "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002" + }, + "step": { + "gateway": { + "data": { + "host": "IP \u5730\u5740", + "name": "\u7f51\u5173\u540d\u79f0", + "token": "API Token" + }, + "description": "\u60a8\u9700\u8981\u83b7\u53d6 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5\uff1ahttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token", + "title": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" + }, + "user": { + "data": { + "gateway": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" + }, + "description": "\u8bf7\u9009\u62e9\u8981\u8fde\u63a5\u7684\u8bbe\u5907\u3002", + "title": "\u5c0f\u7c73 Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json new file mode 100644 index 00000000000..05806b15470 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connect_error": "\u9023\u7dda\u5931\u6557", + "no_device_selected": "\u672a\u9078\u64c7\u8a2d\u5099\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u8a2d\u5099\u3002" + }, + "step": { + "gateway": { + "data": { + "host": "IP \u4f4d\u5740", + "name": "\u7db2\u95dc\u540d\u7a31", + "token": "API \u5bc6\u9470" + }, + "description": "\u5c07\u9700\u8981\u8f38\u5165 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "title": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" + }, + "user": { + "data": { + "gateway": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" + }, + "description": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u8a2d\u5099\u3002", + "title": "\u5c0f\u7c73 Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 416918e6f43..fd144e1edc7 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -25,7 +25,7 @@ from homeassistant.components.vacuum import ( SUPPORT_START, SUPPORT_STATE, SUPPORT_STOP, - StateVacuumDevice, + StateVacuumEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -229,7 +229,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MiroboVacuum(StateVacuumDevice): +class MiroboVacuum(StateVacuumEntity): """Representation of a Xiaomi Vacuum cleaner robot.""" def __init__(self, name, vacuum): diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index c82708852c2..6c0a55f787d 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -4,7 +4,7 @@ import logging import pymitv import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in pymitv.Discover().scan()) -class XiaomiTV(MediaPlayerDevice): +class XiaomiTV(MediaPlayerEntity): """Represent the Xiaomi TV for Home Assistant.""" def __init__(self, ip, name): @@ -87,14 +87,14 @@ class XiaomiTV(MediaPlayerDevice): because the TV won't accept any input when turned off. Thus, the user would be unable to turn the TV back on, unless it's done manually. """ - if self._state is not STATE_OFF: + if self._state != STATE_OFF: self._tv.sleep() self._state = STATE_OFF def turn_on(self): """Wake the TV back up from sleep.""" - if self._state is not STATE_ON: + if self._state != STATE_ON: self._tv.wake() self._state = STATE_ON diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 8f35f813d99..a82e3492599 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,6 +2,6 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.4.2"], + "requirements": ["slixmpp==1.5.1"], "codeowners": ["@fabaff", "@flowolf"] } diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 19d5ae1e904..c57c0857817 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -3,7 +3,7 @@ import logging from xs1_api_client.api_constants import ActuatorType -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(thermostat_entities) -class XS1ThermostatEntity(XS1DeviceEntity, ClimateDevice): +class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): """Representation of a XS1 thermostat.""" def __init__(self, device, sensor): diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 05823a511dd..d3504bcc6da 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -12,7 +12,7 @@ from yalesmartalarmclient.client import ( from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA, - AlarmControlPanel, + AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([YaleAlarmDevice(name, client)], True) -class YaleAlarmDevice(AlarmControlPanel): +class YaleAlarmDevice(AlarmControlPanelEntity): """Represent a Yale Smart Alarm.""" def __init__(self, name, client): diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 5aa0299200a..b26729c720e 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -5,7 +5,7 @@ import requests import rxv import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -153,7 +153,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class YamahaDevice(MediaPlayerDevice): +class YamahaDevice(MediaPlayerEntity): """Representation of a Yamaha device.""" def __init__(self, name, receiver, source_ignore, source_names, zone_names): diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index f239e07a1dc..bfec5b932a8 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -5,7 +5,7 @@ import socket import pymusiccast import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -105,7 +105,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): known_hosts.remove(reg_host) -class YamahaDevice(MediaPlayerDevice): +class YamahaDevice(MediaPlayerEntity): """Representation of a Yamaha MusicCast device.""" def __init__(self, recv, zone): @@ -133,7 +133,7 @@ class YamahaDevice(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - if self.power == STATE_ON and self.status is not STATE_UNKNOWN: + if self.power == STATE_ON and self.status != STATE_UNKNOWN: return self.status return self.power diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index f5f3e03b765..78a20ab0104 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -1,7 +1,7 @@ """Sensor platform support for yeelight.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DATA_UPDATED, DATA_YEELIGHT @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([YeelightNightlightModeSensor(device)]) -class YeelightNightlightModeSensor(BinarySensorDevice): +class YeelightNightlightModeSensor(BinarySensorEntity): """Representation of a Yeelight nightlight mode sensor.""" def __init__(self, device): diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index ab49e46938c..29f943906d6 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -29,7 +29,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, - Light, + LightEntity, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_HOST, CONF_NAME from homeassistant.core import callback @@ -141,6 +141,7 @@ MODEL_TO_DEVICE_TYPE = { "ceiling2": BulbType.WhiteTemp, "ceiling3": BulbType.WhiteTemp, "ceiling4": BulbType.WhiteTempMood, + "ceiling10": BulbType.WhiteTempMood, "ceiling13": BulbType.WhiteTemp, } @@ -426,7 +427,7 @@ def setup_services(hass): ) -class YeelightGenericLight(Light): +class YeelightGenericLight(LightEntity): """Representation of a Yeelight generic light.""" def __init__(self, device, custom_effects=None): diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index c49c874dc21..2e17b92c90a 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - Light, + LightEntity, ) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(SunflowerBulb(light) for light in hub.get_lights()) -class SunflowerBulb(Light): +class SunflowerBulb(LightEntity): """Representation of a Yeelight Sunflower Light.""" def __init__(self, light): diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 42746c6bad3..fa2c7d50add 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import CONF_DEVICES, 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(lights, True) -class ZenggeLight(Light): +class ZenggeLight(LightEntity): """Representation of a Zengge light.""" def __init__(self, device): diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 6fd5d96a40c..d699160eed4 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -5,6 +5,9 @@ import socket import voluptuous as vol from zeroconf import ( + DNSPointer, + DNSRecord, + InterfaceChoice, NonUniqueNameException, ServiceBrowser, ServiceInfo, @@ -20,6 +23,9 @@ from homeassistant.const import ( __version__, ) from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.singleton import singleton _LOGGER = logging.getLogger(__name__) @@ -34,21 +40,107 @@ ATTR_PROPERTIES = "properties" ZEROCONF_TYPE = "_home-assistant._tcp.local." HOMEKIT_TYPE = "_hap._tcp.local." -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +CONF_DEFAULT_INTERFACE = "default_interface" +DEFAULT_DEFAULT_INTERFACE = False + +HOMEKIT_PROPERTIES = "properties" +HOMEKIT_PAIRED_STATUS_FLAG = "sf" +HOMEKIT_MODEL = "md" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional( + CONF_DEFAULT_INTERFACE, default=DEFAULT_DEFAULT_INTERFACE + ): cv.boolean + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +@singleton(DOMAIN) +async def async_get_instance(hass): + """Zeroconf instance to be shared with other integrations that use it.""" + return await hass.async_add_executor_job(_get_instance, hass) + + +def _get_instance(hass, default_interface=False): + """Create an instance.""" + args = [InterfaceChoice.Default] if default_interface else [] + zeroconf = HaZeroconf(*args) + + def stop_zeroconf(_): + """Stop Zeroconf.""" + zeroconf.ha_close() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) + + return zeroconf + + +class HaServiceBrowser(ServiceBrowser): + """ServiceBrowser that only consumes DNSPointer records.""" + + def update_record(self, zc: "Zeroconf", now: float, record: DNSRecord) -> None: + """Pre-Filter update_record to DNSPointers for the configured type.""" + + # + # Each ServerBrowser currently runs in its own thread which + # processes every A or AAAA record update per instance. + # + # As the list of zeroconf names we watch for grows, each additional + # ServiceBrowser would process all the A and AAAA updates on the network. + # + # To avoid overwhemling the system we pre-filter here and only process + # DNSPointers for the configured record name (type) + # + if record.name != self.type or not isinstance(record, DNSPointer): + return + super().update_record(zc, now, record) + + +class HaZeroconf(Zeroconf): + """Zeroconf that cannot be closed.""" + + def close(self): + """Fake method to avoid integrations closing it.""" + + ha_close = Zeroconf.close def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" - zeroconf = Zeroconf() + zeroconf = hass.data[DOMAIN] = _get_instance( + hass, config.get(DOMAIN, {}).get(CONF_DEFAULT_INTERFACE) + ) zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" params = { "version": __version__, - "base_url": hass.config.api.base_url, + "external_url": None, + "internal_url": None, + # Old base URL, for backward compatibility + "base_url": None, # Always needs authentication "requires_api_password": True, } + try: + params["external_url"] = get_url(hass, allow_internal=False) + except NoURLAvailableError: + pass + + try: + params["internal_url"] = get_url(hass, allow_external=False) + except NoURLAvailableError: + pass + + # Set old base URL based on external or internal + params["base_url"] = params["external_url"] or params["internal_url"] + host_ip = util.get_local_ip() try: @@ -86,12 +178,36 @@ def setup(hass, config): return service_info = zeroconf.get_service_info(service_type, name) + if not service_info: + # Prevent the browser thread from collapsing as + # service_info can be None + return + info = info_from_service(service_info) _LOGGER.debug("Discovered new device %s %s", name, info) # If we can handle it as a HomeKit discovery, we do that here. - if service_type == HOMEKIT_TYPE and handle_homekit(hass, info): - return + if service_type == HOMEKIT_TYPE: + handle_homekit(hass, info) + # Continue on here as homekit_controller + # still needs to get updates on devices + # so it can see when the 'c#' field is updated. + # + # We only send updates to homekit_controller + # if the device is already paired in order to avoid + # offering a second discovery for the same device + if ( + HOMEKIT_PROPERTIES in info + and HOMEKIT_PAIRED_STATUS_FLAG in info[HOMEKIT_PROPERTIES] + ): + try: + # 0 means paired and not discoverable by iOS clients) + if int(info[HOMEKIT_PROPERTIES][HOMEKIT_PAIRED_STATUS_FLAG]): + return + except ValueError: + # HomeKit pairing status unknown + # likely bad homekit data + return for domain in ZEROCONF[service_type]: hass.add_job( @@ -101,16 +217,10 @@ def setup(hass, config): ) for service in ZEROCONF: - ServiceBrowser(zeroconf, service, handlers=[service_update]) + HaServiceBrowser(zeroconf, service, handlers=[service_update]) if HOMEKIT_TYPE not in ZEROCONF: - ServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) - - def stop_zeroconf(_): - """Stop Zeroconf.""" - zeroconf.close() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) + HaServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update]) return True @@ -121,10 +231,10 @@ def handle_homekit(hass, info) -> bool: Return if discovery was forwarded. """ model = None - props = info.get("properties", {}) + props = info.get(HOMEKIT_PROPERTIES, {}) for key in props: - if key.lower() == "md": + if key.lower() == HOMEKIT_MODEL: model = props[key] break diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 241cf443244..a3d4d1d8399 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.25.1"], + "requirements": ["zeroconf==0.26.1"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py new file mode 100644 index 00000000000..2c652f61c21 --- /dev/null +++ b/homeassistant/components/zerproc/__init__.py @@ -0,0 +1,40 @@ +"""Zerproc lights integration.""" +import asyncio + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = ["light"] + + +async def async_setup(hass, config): + """Set up the Zerproc platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_IMPORT}) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Zerproc from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + return all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py new file mode 100644 index 00000000000..28597b3859e --- /dev/null +++ b/homeassistant/components/zerproc/config_flow.py @@ -0,0 +1,26 @@ +"""Config flow for Zerproc.""" +import logging + +import pyzerproc + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + try: + devices = await hass.async_add_executor_job(pyzerproc.discover) + return len(devices) > 0 + except pyzerproc.ZerprocException: + _LOGGER.error("Unable to discover nearby Zerproc devices", exc_info=True) + return False + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Zerproc", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL +) diff --git a/homeassistant/components/zerproc/const.py b/homeassistant/components/zerproc/const.py new file mode 100644 index 00000000000..a5481bd4c34 --- /dev/null +++ b/homeassistant/components/zerproc/const.py @@ -0,0 +1,2 @@ +"""Constants for the Zerproc integration.""" +DOMAIN = "zerproc" diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py new file mode 100644 index 00000000000..0a7141efe8a --- /dev/null +++ b/homeassistant/components/zerproc/light.py @@ -0,0 +1,203 @@ +"""Zerproc light platform.""" +from datetime import timedelta +import logging +from typing import Callable, List + +import pyzerproc + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_ZERPROC = SUPPORT_BRIGHTNESS | SUPPORT_COLOR + +DISCOVERY_INTERVAL = timedelta(seconds=60) + +PARALLEL_UPDATES = 0 + + +def connect_lights(lights: List[pyzerproc.Light]) -> List[pyzerproc.Light]: + """Attempt to connect to lights, and return the connected lights.""" + connected = [] + for light in lights: + try: + light.connect(auto_reconnect=True) + connected.append(light) + except pyzerproc.ZerprocException: + _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) + + return connected + + +def discover_entities(hass: HomeAssistant) -> List[Entity]: + """Attempt to discover new lights.""" + lights = pyzerproc.discover() + + # Filter out already discovered lights + new_lights = [ + light for light in lights if light.address not in hass.data[DOMAIN]["addresses"] + ] + + entities = [] + for light in connect_lights(new_lights): + # Double-check the light hasn't been added in another thread + if light.address not in hass.data[DOMAIN]["addresses"]: + hass.data[DOMAIN]["addresses"].add(light.address) + entities.append(ZerprocLight(light)) + + return entities + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Abode light devices.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if "addresses" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["addresses"] = set() + + warned = False + + async def discover(*args): + """Wrap discovery to include params.""" + nonlocal warned + try: + entities = await hass.async_add_executor_job(discover_entities, hass) + async_add_entities(entities, update_before_add=True) + warned = False + except pyzerproc.ZerprocException: + if warned is False: + _LOGGER.warning("Error discovering Zerproc lights", exc_info=True) + warned = True + + # Initial discovery + hass.async_create_task(discover()) + + # Perform recurring discovery of new devices + async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + + +class ZerprocLight(Light): + """Representation of an Zerproc Light.""" + + def __init__(self, light): + """Initialize a Zerproc light.""" + self._light = light + self._name = None + self._is_on = None + self._hs_color = None + self._brightness = None + self._available = True + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.on_hass_shutdown + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + await self.hass.async_add_executor_job(self._light.disconnect) + + def on_hass_shutdown(self, event): + """Execute when Home Assistant is shutting down.""" + self._light.disconnect() + + @property + def name(self): + """Return the display name of this light.""" + return self._light.name + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._light.address + + @property + def device_info(self): + """Device info for this light.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Zerproc", + } + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_ZERPROC + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the hs color.""" + return self._hs_color + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: + default_hs = (0, 0) if self._hs_color is None else self._hs_color + hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + + default_brightness = 255 if self._brightness is None else self._brightness + brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) + + rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + self._light.set_color(*rgb) + else: + self._light.turn_on() + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.turn_off() + + def update(self): + """Fetch new state data for this light.""" + try: + state = self._light.get_state() + except pyzerproc.ZerprocException: + if self._available: + _LOGGER.warning("Unable to connect to %s", self.entity_id) + self._available = False + return + if self._available is False: + _LOGGER.info("Reconnected to %s", self.entity_id) + self._available = True + self._is_on = state.is_on + hsv = color_util.color_RGB_to_hsv(*state.color) + self._hs_color = hsv[:2] + self._brightness = int(round((hsv[2] / 100) * 255)) diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json new file mode 100644 index 00000000000..f00a8bdc885 --- /dev/null +++ b/homeassistant/components/zerproc/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "zerproc", + "name": "Zerproc", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zerproc", + "requirements": [ + "pyzerproc==0.2.4" + ], + "codeowners": [ + "@emlove" + ] +} diff --git a/homeassistant/components/zerproc/strings.json b/homeassistant/components/zerproc/strings.json new file mode 100644 index 00000000000..9662bdc36d8 --- /dev/null +++ b/homeassistant/components/zerproc/strings.json @@ -0,0 +1,14 @@ +{ + "title": "Zerproc", + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/zerproc/translations/ca.json b/homeassistant/components/zerproc/translations/ca.json new file mode 100644 index 00000000000..dc21c371e60 --- /dev/null +++ b/homeassistant/components/zerproc/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/en.json b/homeassistant/components/zerproc/translations/en.json new file mode 100644 index 00000000000..f88874372c8 --- /dev/null +++ b/homeassistant/components/zerproc/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/he.json b/homeassistant/components/zerproc/translations/he.json new file mode 100644 index 00000000000..e228b3719d1 --- /dev/null +++ b/homeassistant/components/zerproc/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8. \u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d7\u05d9\u05d3\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/ko.json b/homeassistant/components/zerproc/translations/ko.json new file mode 100644 index 00000000000..641f498c3b3 --- /dev/null +++ b/homeassistant/components/zerproc/translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/no.json b/homeassistant/components/zerproc/translations/no.json new file mode 100644 index 00000000000..cdfd3890fb8 --- /dev/null +++ b/homeassistant/components/zerproc/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/ru.json b/homeassistant/components/zerproc/translations/ru.json new file mode 100644 index 00000000000..438c1f2df58 --- /dev/null +++ b/homeassistant/components/zerproc/translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/zh-Hant.json b/homeassistant/components/zerproc/translations/zh-Hant.json new file mode 100644 index 00000000000..720d3f293ee --- /dev/null +++ b/homeassistant/components/zerproc/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 9e59b63adb4..8a23c6fc20d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -4,6 +4,7 @@ import asyncio import logging import voluptuous as vol +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries, const as ha_const import homeassistant.helpers.config_validation as cv @@ -14,6 +15,7 @@ from homeassistant.helpers.typing import HomeAssistantType from . import api from .core import ZHAGateway from .core.const import ( + BAUD_RATES, COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, @@ -21,13 +23,12 @@ from .core.const import ( CONF_ENABLE_QUIRKS, CONF_RADIO_TYPE, CONF_USB_PATH, + CONF_ZIGPY, DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DATA_ZHA_PLATFORM_LOADED, - DEFAULT_BAUDRATE, - DEFAULT_RADIO_TYPE, DOMAIN, SIGNAL_ADD_ENTITIES, RadioType, @@ -35,23 +36,27 @@ from .core.const import ( from .core.discovery import GROUP_PROBE DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) - +ZHA_CONFIG_SCHEMA = { + vol.Optional(CONF_BAUDRATE): cv.positive_int, + vol.Optional(CONF_DATABASE): cv.string, + vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( + {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} + ), + vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean, + vol.Optional(CONF_ZIGPY): dict, + vol.Optional(CONF_RADIO_TYPE): cv.enum(RadioType), + vol.Optional(CONF_USB_PATH): cv.string, +} CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Optional(CONF_RADIO_TYPE, default=DEFAULT_RADIO_TYPE): cv.enum( - RadioType - ), - CONF_USB_PATH: cv.string, - vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, - vol.Optional(CONF_DATABASE): cv.string, - vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( - {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean, - } - ) + vol.All( + cv.deprecated(CONF_USB_PATH, invalidation_version="0.112"), + cv.deprecated(CONF_BAUDRATE, invalidation_version="0.112"), + cv.deprecated(CONF_RADIO_TYPE, invalidation_version="0.112"), + ZHA_CONFIG_SCHEMA, + ), + ), }, extra=vol.ALLOW_EXTRA, ) @@ -67,23 +72,10 @@ async def async_setup(hass, config): """Set up ZHA from config.""" hass.data[DATA_ZHA] = {} - if DOMAIN not in config: - return True + if DOMAIN in config: + conf = config[DOMAIN] + hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = conf - conf = config[DOMAIN] - hass.data[DATA_ZHA][DATA_ZHA_CONFIG] = conf - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_USB_PATH: conf[CONF_USB_PATH], - CONF_RADIO_TYPE: conf.get(CONF_RADIO_TYPE).value, - }, - ) - ) return True @@ -161,3 +153,26 @@ async def async_load_entities(hass: HomeAssistantType) -> None: if isinstance(res, Exception): _LOGGER.warning("Couldn't setup zha platform: %s", res) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) + + +async def async_migrate_entry( + hass: HomeAssistantType, config_entry: config_entries.ConfigEntry +): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + data = { + CONF_RADIO_TYPE: config_entry.data[CONF_RADIO_TYPE], + CONF_DEVICE: {CONF_DEVICE_PATH: config_entry.data[CONF_USB_PATH]}, + } + + baudrate = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}).get(CONF_BAUDRATE) + if data[CONF_RADIO_TYPE] != RadioType.deconz and baudrate in BAUD_RATES: + data[CONF_DEVICE][CONF_BAUDRATE] = baudrate + + config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=data) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + return True diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 232a5666300..1ba9ada5413 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -53,6 +53,7 @@ from .core.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) +from .core.group import GroupMember from .core.helpers import async_is_bindable_target, get_matched_clusters _LOGGER = logging.getLogger(__name__) @@ -209,7 +210,7 @@ async def websocket_get_devices(hass, connection, msg): """Get ZHA devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - devices = [device.async_get_info() for device in zha_gateway.devices.values()] + devices = [device.zha_device_info for device in zha_gateway.devices.values()] connection.send_result(msg[ID], devices) @@ -221,13 +222,35 @@ async def websocket_get_groupable_devices(hass, connection, msg): """Get ZHA devices that can be grouped.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - devices = [ - device.async_get_info() - for device in zha_gateway.devices.values() - if device.is_groupable or device.is_coordinator - ] + devices = [device for device in zha_gateway.devices.values() if device.is_groupable] + groupable_devices = [] - connection.send_result(msg[ID], devices) + for device in devices: + entity_refs = zha_gateway.device_registry.get(device.ieee) + for ep_id in device.async_get_groupable_endpoints(): + groupable_devices.append( + { + "endpoint_id": ep_id, + "entities": [ + { + "name": zha_gateway.ha_entity_registry.async_get( + entity_ref.reference_id + ).name, + "original_name": zha_gateway.ha_entity_registry.async_get( + entity_ref.reference_id + ).original_name, + } + for entity_ref in entity_refs + if list(entity_ref.cluster_channels.values())[ + 0 + ].cluster.endpoint.endpoint_id + == ep_id + ], + "device": device.zha_device_info, + } + ) + + connection.send_result(msg[ID], groupable_devices) @websocket_api.require_admin @@ -236,7 +259,7 @@ async def websocket_get_groupable_devices(hass, connection, msg): async def websocket_get_groups(hass, connection, msg): """Get ZHA groups.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - groups = [group.async_get_info() for group in zha_gateway.groups.values()] + groups = [group.group_info for group in zha_gateway.groups.values()] connection.send_result(msg[ID], groups) @@ -251,7 +274,7 @@ async def websocket_get_device(hass, connection, msg): ieee = msg[ATTR_IEEE] device = None if ieee in zha_gateway.devices: - device = zha_gateway.devices[ieee].async_get_info() + device = zha_gateway.devices[ieee].zha_device_info if not device: connection.send_message( websocket_api.error_message( @@ -274,7 +297,7 @@ async def websocket_get_group(hass, connection, msg): group = None if group_id in zha_gateway.groups: - group = zha_gateway.groups.get(group_id).async_get_info() + group = zha_gateway.groups.get(group_id).group_info if not group: connection.send_message( websocket_api.error_message( @@ -285,13 +308,27 @@ async def websocket_get_group(hass, connection, msg): connection.send_result(msg[ID], group) +def cv_group_member(value: Any) -> GroupMember: + """Validate and transform a group member.""" + if not isinstance(value, Mapping): + raise vol.Invalid("Not a group member") + try: + group_member = GroupMember( + ieee=EUI64.convert(value["ieee"]), endpoint_id=value["endpoint_id"] + ) + except KeyError: + raise vol.Invalid("Not a group member") + + return group_member + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/add", vol.Required(GROUP_NAME): cv.string, - vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) async def websocket_add_group(hass, connection, msg): @@ -300,7 +337,7 @@ async def websocket_add_group(hass, connection, msg): group_name = msg[GROUP_NAME] members = msg.get(ATTR_MEMBERS) group = await zha_gateway.async_create_zigpy_group(group_name, members) - connection.send_result(msg[ID], group.async_get_info()) + connection.send_result(msg[ID], group.group_info) @websocket_api.require_admin @@ -323,7 +360,7 @@ async def websocket_remove_groups(hass, connection, msg): await asyncio.gather(*tasks) else: await zha_gateway.async_remove_zigpy_group(group_ids[0]) - ret_groups = [group.async_get_info() for group in zha_gateway.groups.values()] + ret_groups = [group.group_info for group in zha_gateway.groups.values()] connection.send_result(msg[ID], ret_groups) @@ -333,7 +370,7 @@ async def websocket_remove_groups(hass, connection, msg): { vol.Required(TYPE): "zha/group/members/add", vol.Required(GROUP_ID): cv.positive_int, - vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) async def websocket_add_group_members(hass, connection, msg): @@ -353,7 +390,7 @@ async def websocket_add_group_members(hass, connection, msg): ) ) return - ret_group = zha_group.async_get_info() + ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @@ -363,7 +400,7 @@ async def websocket_add_group_members(hass, connection, msg): { vol.Required(TYPE): "zha/group/members/remove", vol.Required(GROUP_ID): cv.positive_int, - vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) async def websocket_remove_group_members(hass, connection, msg): @@ -383,7 +420,7 @@ async def websocket_remove_group_members(hass, connection, msg): ) ) return - ret_group = zha_group.async_get_info() + ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @@ -608,7 +645,7 @@ async def websocket_get_bindable_devices(hass, connection, msg): source_device = zha_gateway.get_device(source_ieee) devices = [ - device.async_get_info() + device.zha_device_info for device in zha_gateway.devices.values() if async_is_bindable_target(source_device, device) ] diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 044d32890da..0ed931e92da 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, DOMAIN, - BinarySensorDevice, + BinarySensorEntity, ) from homeassistant.const import STATE_ON from homeassistant.core import callback @@ -61,7 +61,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class BinarySensor(ZhaEntity, BinarySensorDevice): +class BinarySensor(ZhaEntity, BinarySensorEntity): """ZHA BinarySensor.""" SENSOR_ATTR = None diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 5ee0d0ee9bb..e3a2044454a 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,80 +1,139 @@ """Config flow for ZHA.""" -import asyncio -from collections import OrderedDict import os +from typing import Any, Dict, Optional +import serial.tools.list_ports import voluptuous as vol +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries -from .core.const import ( +from .core.const import ( # pylint:disable=unused-import + CONF_BAUDRATE, + CONF_FLOWCONTROL, CONF_RADIO_TYPE, - CONF_USB_PATH, - CONTROLLER, - DEFAULT_BAUDRATE, - DEFAULT_DATABASE_NAME, DOMAIN, - ZHA_GW_RADIO, RadioType, ) -from .core.registries import RADIO_TYPES + +CONF_MANUAL_PATH = "Enter Manually" +SUPPORTED_PORT_SETTINGS = ( + CONF_BAUDRATE, + CONF_FLOWCONTROL, +) -@config_entries.HANDLERS.register(DOMAIN) -class ZhaFlowHandler(config_entries.ConfigFlow): +class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + def __init__(self): + """Initialize flow instance.""" + self._device_path = None + self._radio_type = None + async def async_step_user(self, user_input=None): """Handle a zha config flow start.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - errors = {} - - fields = OrderedDict() - fields[vol.Required(CONF_USB_PATH)] = str - fields[vol.Optional(CONF_RADIO_TYPE, default="ezsp")] = vol.In(RadioType.list()) + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + list_of_ports = [ + f"{p}, s/n: {p.serial_number or 'n/a'}" + + (f" - {p.manufacturer}" if p.manufacturer else "") + for p in ports + ] + list_of_ports.append(CONF_MANUAL_PATH) if user_input is not None: - database = os.path.join(self.hass.config.config_dir, DEFAULT_DATABASE_NAME) - test = await check_zigpy_connection( - user_input[CONF_USB_PATH], user_input[CONF_RADIO_TYPE], database + user_selection = user_input[CONF_DEVICE_PATH] + if user_selection == CONF_MANUAL_PATH: + return await self.async_step_pick_radio() + + port = ports[list_of_ports.index(user_selection)] + dev_path = await self.hass.async_add_executor_job( + get_serial_by_id, port.device ) - if test: + auto_detected_data = await detect_radios(dev_path) + if auto_detected_data is not None: + title = f"{port.description}, s/n: {port.serial_number or 'n/a'}" + title += f" - {port.manufacturer}" if port.manufacturer else "" + return self.async_create_entry(title=title, data=auto_detected_data,) + + # did not detect anything + self._device_path = dev_path + return await self.async_step_pick_radio() + + schema = vol.Schema({vol.Required(CONF_DEVICE_PATH): vol.In(list_of_ports)}) + return self.async_show_form(step_id="user", data_schema=schema) + + async def async_step_pick_radio(self, user_input=None): + """Select radio type.""" + + if user_input is not None: + self._radio_type = RadioType.get_by_description(user_input[CONF_RADIO_TYPE]) + return await self.async_step_port_config() + + schema = {vol.Required(CONF_RADIO_TYPE): vol.In(sorted(RadioType.list()))} + return self.async_show_form( + step_id="pick_radio", data_schema=vol.Schema(schema), + ) + + async def async_step_port_config(self, user_input=None): + """Enter port settings specific for this type of radio.""" + errors = {} + app_cls = RadioType[self._radio_type].controller + + if user_input is not None: + self._device_path = user_input.get(CONF_DEVICE_PATH) + if await app_cls.probe(user_input): + serial_by_id = await self.hass.async_add_executor_job( + get_serial_by_id, user_input[CONF_DEVICE_PATH] + ) + user_input[CONF_DEVICE_PATH] = serial_by_id return self.async_create_entry( - title=user_input[CONF_USB_PATH], data=user_input + title=user_input[CONF_DEVICE_PATH], + data={CONF_DEVICE: user_input, CONF_RADIO_TYPE: self._radio_type}, ) errors["base"] = "cannot_connect" + schema = { + vol.Required( + CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED + ): str + } + radio_schema = app_cls.SCHEMA_DEVICE.schema + if isinstance(radio_schema, vol.Schema): + radio_schema = radio_schema.schema + + for param, value in radio_schema.items(): + if param in SUPPORTED_PORT_SETTINGS: + schema[param] = value + return self.async_show_form( - step_id="user", data_schema=vol.Schema(fields), errors=errors - ) - - async def async_step_import(self, import_info): - """Handle a zha config import.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return self.async_create_entry( - title=import_info[CONF_USB_PATH], data=import_info + step_id="port_config", data_schema=vol.Schema(schema), errors=errors, ) -async def check_zigpy_connection(usb_path, radio_type, database_path): - """Test zigpy radio connection.""" - try: - radio = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() - controller_application = RADIO_TYPES[radio_type][CONTROLLER] - except KeyError: - return False - try: - await radio.connect(usb_path, DEFAULT_BAUDRATE) - controller = controller_application(radio, database_path) - await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) - await controller.shutdown() - except Exception: # pylint: disable=broad-except - return False - return True +async def detect_radios(dev_path: str) -> Optional[Dict[str, Any]]: + """Probe all radio types on the device port.""" + for radio in RadioType: + dev_config = radio.controller.SCHEMA_DEVICE({CONF_DEVICE_PATH: dev_path}) + if await radio.controller.probe(dev_config): + return {CONF_RADIO_TYPE: radio.name, CONF_DEVICE: dev_config} + + return None + + +def get_serial_by_id(dev_path: str) -> str: + """Return a /dev/serial/by-id match for given device if available.""" + by_id = "/dev/serial/by-id" + if not os.path.isdir(by_id): + return dev_path + + for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): + if os.path.realpath(path) == dev_path: + return path + return dev_path diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index b181b848f04..08252ab1c97 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -1,6 +1,14 @@ """All constants related to the ZHA component.""" import enum import logging +from typing import List + +import bellows.zigbee.application +from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import +import zigpy_cc.zigbee.application +import zigpy_deconz.zigbee.application +import zigpy_xbee.zigbee.application +import zigpy_zigate.zigbee.application from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.cover import DOMAIN as COVER @@ -11,6 +19,8 @@ from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +from .typing import CALLABLE_T + ATTR_ARGS = "args" ATTR_ATTRIBUTE = "attribute" ATTR_ATTRIBUTE_ID = "attribute_id" @@ -92,9 +102,10 @@ CONF_BAUDRATE = "baudrate" CONF_DATABASE = "database_path" CONF_DEVICE_CONFIG = "device_config" CONF_ENABLE_QUIRKS = "enable_quirks" +CONF_FLOWCONTROL = "flow_control" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" -CONTROLLER = "controller" +CONF_ZIGPY = "zigpy_config" DATA_DEVICE_CONFIG = "zha_device_config" DATA_ZHA = "zha" @@ -145,16 +156,51 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" class RadioType(enum.Enum): """Possible options for radio type.""" - deconz = "deconz" - ezsp = "ezsp" - ti_cc = "ti_cc" - xbee = "xbee" - zigate = "zigate" + ezsp = ( + "ESZP: HUSBZB-1, Elelabs, Telegesis, Silabs EmberZNet protocol", + bellows.zigbee.application.ControllerApplication, + ) + deconz = ( + "Conbee, Conbee II, RaspBee radios from dresden elektronik", + zigpy_deconz.zigbee.application.ControllerApplication, + ) + ti_cc = ( + "TI_CC: CC2531, CC2530, CC2652R, CC1352 etc, Texas Instruments ZNP protocol", + zigpy_cc.zigbee.application.ControllerApplication, + ) + zigate = "ZiGate Radio", zigpy_zigate.zigbee.application.ControllerApplication + xbee = ( + "Digi XBee S2C, XBee 3 radios", + zigpy_xbee.zigbee.application.ControllerApplication, + ) @classmethod - def list(cls): - """Return list of enum's values.""" - return [e.value for e in RadioType] + def list(cls) -> List[str]: + """Return a list of descriptions.""" + return [e.description for e in RadioType] + + @classmethod + def get_by_description(cls, description: str) -> str: + """Get radio by description.""" + for radio in cls: + if radio.description == description: + return radio.name + raise ValueError + + def __init__(self, description: str, controller_cls: CALLABLE_T): + """Init instance.""" + self._desc = description + self._ctrl_cls = controller_cls + + @property + def controller(self) -> CALLABLE_T: + """Return controller class.""" + return self._ctrl_cls + + @property + def description(self) -> str: + """Return radio type description.""" + return self._desc REPORT_CONFIG_MAX_INT = 900 @@ -258,8 +304,6 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" -ZHA_GW_RADIO = "radio" -ZHA_GW_RADIO_DESCRIPTION = "radio_description" EFFECT_BLINK = 0x00 EFFECT_BREATHE = 0x01 diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index b4947d121e4..fcbf518a9db 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -235,13 +235,9 @@ class ZHADevice(LogMixin): @property def is_groupable(self): """Return true if this device has a group cluster.""" - if not self.available: - return False - clusters = self.async_get_clusters() - for cluster_map in clusters.values(): - for clusters in cluster_map.values(): - if Groups.cluster_id in clusters: - return True + return self.is_coordinator or ( + self.available and self.async_get_groupable_endpoints() + ) @property def skip_configuration(self): @@ -411,8 +407,8 @@ class ZHADevice(LogMixin): if self._zigpy_device.last_seen is None and last_seen is not None: self._zigpy_device.last_seen = last_seen - @callback - def async_get_info(self): + @property + def zha_device_info(self): """Get ZHA device information.""" device_info = {} device_info.update(self.device_info) @@ -442,6 +438,15 @@ class ZHADevice(LogMixin): if ep_id != 0 } + @callback + def async_get_groupable_endpoints(self): + """Get device endpoints that have a group 'in' cluster.""" + return [ + ep_id + for (ep_id, clusters) in self.async_get_clusters().items() + if Groups.cluster_id in clusters[CLUSTER_TYPE_IN] + ] + @callback def async_get_std_clusters(self): """Get ZHA and ZLL clusters for this device.""" @@ -557,7 +562,15 @@ class ZHADevice(LogMixin): async def async_add_to_group(self, group_id): """Add this device to the provided zigbee group.""" - await self._zigpy_device.add_to_group(group_id) + try: + await self._zigpy_device.add_to_group(group_id) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to add device '%s' to group: 0x%04x ex: %s", + self._zigpy_device.ieee, + group_id, + str(ex), + ) async def async_remove_from_group(self, group_id): """Remove this device from the provided zigbee group.""" @@ -571,6 +584,34 @@ class ZHADevice(LogMixin): str(ex), ) + async def async_add_endpoint_to_group(self, endpoint_id, group_id): + """Add the device endpoint to the provided zigbee group.""" + try: + await self._zigpy_device.endpoints[int(endpoint_id)].add_to_group(group_id) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s", + endpoint_id, + self._zigpy_device.ieee, + group_id, + str(ex), + ) + + async def async_remove_endpoint_from_group(self, endpoint_id, group_id): + """Remove the device endpoint from the provided zigbee group.""" + try: + await self._zigpy_device.endpoints[int(endpoint_id)].remove_from_group( + group_id + ) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s", + endpoint_id, + self._zigpy_device.ieee, + group_id, + str(ex), + ) + async def async_bind_to_group(self, group_id, cluster_bindings): """Directly bind this device to a group for the given clusters.""" await self._async_group_binding_operation( diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 4540c9158de..f72ac2161ec 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -235,21 +235,13 @@ class GroupProbe: ) -> List[str]: """Determine the entity domains for this group.""" entity_domains: List[str] = [] - if len(group.members) < 2: - _LOGGER.debug( - "Group: %s:0x%04x has less than 2 members so cannot default an entity domain", - group.name, - group.group_id, - ) - return entity_domains - zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] all_domain_occurrences = [] - for device in group.members: - if device.is_coordinator: + for member in group.members: + if member.device.is_coordinator: continue entities = async_entries_for_device( - zha_gateway.ha_entity_registry, device.device_id + zha_gateway.ha_entity_registry, member.device.device_id ) all_domain_occurrences.extend( [ diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index e97e2185dc5..08f412dfcd8 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -10,6 +10,7 @@ import traceback from typing import List, Optional from serial import SerialException +from zigpy.config import CONF_DEVICE import zigpy.device as zigpy_dev from homeassistant.components.system_log import LogEntry, _figure_out_source @@ -33,11 +34,9 @@ from .const import ( ATTR_NWK, ATTR_SIGNATURE, ATTR_TYPE, - CONF_BAUDRATE, CONF_DATABASE, CONF_RADIO_TYPE, - CONF_USB_PATH, - CONTROLLER, + CONF_ZIGPY, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, @@ -52,7 +51,6 @@ from .const import ( DEBUG_LEVEL_ORIGINAL, DEBUG_LEVELS, DEBUG_RELAY_LOGGERS, - DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DOMAIN, SIGNAL_ADD_ENTITIES, @@ -74,15 +72,14 @@ from .const import ( ZHA_GW_MSG_LOG_ENTRY, ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_RAW_INIT, - ZHA_GW_RADIO, - ZHA_GW_RADIO_DESCRIPTION, + RadioType, ) from .device import DeviceStatus, ZHADevice -from .group import ZHAGroup +from .group import GroupMember, ZHAGroup from .patches import apply_application_controller_patch -from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES +from .registries import GROUP_ENTITY_DOMAINS from .store import async_get_registry -from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType +from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType _LOGGER = logging.getLogger(__name__) @@ -125,43 +122,35 @@ class ZHAGateway: self.ha_device_registry = await get_dev_reg(self._hass) self.ha_entity_registry = await get_ent_reg(self._hass) - usb_path = self._config_entry.data.get(CONF_USB_PATH) - baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) - radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) + radio_type = self._config_entry.data[CONF_RADIO_TYPE] - radio_details = RADIO_TYPES[radio_type] - radio = radio_details[ZHA_GW_RADIO]() - self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION] + app_controller_cls = RadioType[radio_type].controller + self.radio_description = RadioType[radio_type].description + + app_config = self._config.get(CONF_ZIGPY, {}) + database = self._config.get( + CONF_DATABASE, + os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME), + ) + app_config[CONF_DATABASE] = database + app_config[CONF_DEVICE] = self._config_entry.data[CONF_DEVICE] + + app_config = app_controller_cls.SCHEMA(app_config) try: - await radio.connect(usb_path, baudrate) - except (SerialException, OSError) as exception: - _LOGGER.error("Couldn't open serial port for ZHA: %s", str(exception)) - raise ConfigEntryNotReady + self.application_controller = await app_controller_cls.new( + app_config, auto_form=True, start_radio=True + ) + except (asyncio.TimeoutError, SerialException, OSError) as exception: + _LOGGER.error( + "Couldn't start %s coordinator", + self.radio_description, + exc_info=exception, + ) + raise ConfigEntryNotReady from exception - if CONF_DATABASE in self._config: - database = self._config[CONF_DATABASE] - else: - database = os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME) - - self.application_controller = radio_details[CONTROLLER](radio, database) apply_application_controller_patch(self) self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) - - try: - res = await self.application_controller.startup(auto_form=True) - if res is False: - await self.application_controller.shutdown() - raise ConfigEntryNotReady - except asyncio.TimeoutError as exception: - _LOGGER.error( - "Couldn't start %s coordinator", - radio_details[ZHA_GW_RADIO_DESCRIPTION], - exc_info=exception, - ) - radio.close() - raise ConfigEntryNotReady from exception - self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( self.application_controller.ieee @@ -308,7 +297,7 @@ class ZHAGateway: ZHA_GW_MSG, { ATTR_TYPE: gateway_message_type, - ZHA_GW_MSG_GROUP_INFO: zha_group.async_get_info(), + ZHA_GW_MSG_GROUP_INFO: zha_group.group_info, }, ) @@ -327,7 +316,7 @@ class ZHAGateway: zha_device = self._devices.pop(device.ieee, None) entity_refs = self._device_registry.pop(device.ieee, None) if zha_device is not None: - device_info = zha_device.async_get_info() + device_info = zha_device.zha_device_info zha_device.async_cleanup_handles() async_dispatcher_send( self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee)) @@ -542,7 +531,7 @@ class ZHAGateway: ) await self._async_device_joined(zha_device) - device_info = zha_device.async_get_info() + device_info = zha_device.zha_device_info async_dispatcher_send( self._hass, @@ -571,11 +560,11 @@ class ZHAGateway: zha_device.update_available(True) async def async_create_zigpy_group( - self, name: str, members: List[ZhaDeviceType] + self, name: str, members: List[GroupMember] ) -> ZhaGroupType: """Create a new Zigpy Zigbee group.""" - # we start with one to fill any gaps from a user removing existing groups - group_id = 1 + # we start with two to fill any gaps from a user removing existing groups + group_id = 2 while group_id in self.groups: group_id += 1 @@ -584,14 +573,19 @@ class ZHAGateway: self.application_controller.groups.add_group(group_id, name) if members is not None: tasks = [] - for ieee in members: + for member in members: _LOGGER.debug( - "Adding member with IEEE: %s to group: %s:0x%04x", - ieee, + "Adding member with IEEE: %s and endpoint id: %s to group: %s:0x%04x", + member.ieee, + member.endpoint_id, name, group_id, ) - tasks.append(self.devices[ieee].async_add_to_group(group_id)) + tasks.append( + self.devices[member.ieee].async_add_endpoint_to_group( + member.endpoint_id, group_id + ) + ) await asyncio.gather(*tasks) return self.groups.get(group_id) @@ -604,7 +598,7 @@ class ZHAGateway: if group and group.members: tasks = [] for member in group.members: - tasks.append(member.async_remove_from_group(group_id)) + tasks.append(member.async_remove_from_group()) if tasks: await asyncio.gather(*tasks) self.application_controller.groups.pop(group_id) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 4fc86012d1a..2961f335989 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -1,19 +1,110 @@ """Group for Zigbee Home Automation.""" import asyncio +import collections import logging from typing import Any, Dict, List -from zigpy.types.named import EUI64 +import zigpy.exceptions -from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import HomeAssistantType from .helpers import LogMixin -from .typing import ZhaDeviceType, ZhaGatewayType, ZigpyEndpointType, ZigpyGroupType +from .typing import ( + ZhaDeviceType, + ZhaGatewayType, + ZhaGroupType, + ZigpyEndpointType, + ZigpyGroupType, +) _LOGGER = logging.getLogger(__name__) +GroupMember = collections.namedtuple("GroupMember", "ieee endpoint_id") +GroupEntityReference = collections.namedtuple( + "GroupEntityReference", "name original_name entity_id" +) + + +class ZHAGroupMember(LogMixin): + """Composite object that represents a device endpoint in a Zigbee group.""" + + def __init__( + self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int + ): + """Initialize the group member.""" + self._zha_group: ZhaGroupType = zha_group + self._zha_device: ZhaDeviceType = zha_device + self._endpoint_id: int = endpoint_id + + @property + def group(self) -> ZhaGroupType: + """Return the group this member belongs to.""" + return self._zha_group + + @property + def endpoint_id(self) -> int: + """Return the endpoint id for this group member.""" + return self._endpoint_id + + @property + def endpoint(self) -> ZigpyEndpointType: + """Return the endpoint for this group member.""" + return self._zha_device.device.endpoints.get(self.endpoint_id) + + @property + def device(self) -> ZhaDeviceType: + """Return the zha device for this group member.""" + return self._zha_device + + @property + def member_info(self) -> Dict[str, Any]: + """Get ZHA group info.""" + member_info: Dict[str, Any] = {} + member_info["endpoint_id"] = self.endpoint_id + member_info["device"] = self.device.zha_device_info + member_info["entities"] = self.associated_entities + return member_info + + @property + def associated_entities(self) -> List[GroupEntityReference]: + """Return the list of entities that were derived from this endpoint.""" + ha_entity_registry = self.device.gateway.ha_entity_registry + zha_device_registry = self.device.gateway.device_registry + return [ + GroupEntityReference( + ha_entity_registry.async_get(entity_ref.reference_id).name, + ha_entity_registry.async_get(entity_ref.reference_id).original_name, + entity_ref.reference_id, + )._asdict() + for entity_ref in zha_device_registry.get(self.device.ieee) + if list(entity_ref.cluster_channels.values())[ + 0 + ].cluster.endpoint.endpoint_id + == self.endpoint_id + ] + + async def async_remove_from_group(self) -> None: + """Remove the device endpoint from the provided zigbee group.""" + try: + await self._zha_device.device.endpoints[ + self._endpoint_id + ].remove_from_group(self._zha_group.group_id) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s", + self._endpoint_id, + self._zha_device.ieee, + self._zha_group.group_id, + str(ex), + ) + + def log(self, level: int, msg: str, *args) -> None: + """Log a message.""" + msg = f"[%s](%s): {msg}" + args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args + _LOGGER.log(level, msg, *args) + class ZHAGroup(LogMixin): """ZHA Zigbee group object.""" @@ -45,77 +136,79 @@ class ZHAGroup(LogMixin): return self._zigpy_group.endpoint @property - def members(self) -> List[ZhaDeviceType]: + def members(self) -> List[ZHAGroupMember]: """Return the ZHA devices that are members of this group.""" return [ - self._zha_gateway.devices.get(member_ieee[0]) - for member_ieee in self._zigpy_group.members.keys() - if member_ieee[0] in self._zha_gateway.devices + ZHAGroupMember( + self, self._zha_gateway.devices.get(member_ieee), endpoint_id + ) + for (member_ieee, endpoint_id) in self._zigpy_group.members.keys() + if member_ieee in self._zha_gateway.devices ] - async def async_add_members(self, member_ieee_addresses: List[EUI64]) -> None: + async def async_add_members(self, members: List[GroupMember]) -> None: """Add members to this group.""" - if len(member_ieee_addresses) > 1: + if len(members) > 1: tasks = [] - for ieee in member_ieee_addresses: + for member in members: tasks.append( - self._zha_gateway.devices[ieee].async_add_to_group(self.group_id) - ) - await asyncio.gather(*tasks) - else: - await self._zha_gateway.devices[ - member_ieee_addresses[0] - ].async_add_to_group(self.group_id) - - async def async_remove_members(self, member_ieee_addresses: List[EUI64]) -> None: - """Remove members from this group.""" - if len(member_ieee_addresses) > 1: - tasks = [] - for ieee in member_ieee_addresses: - tasks.append( - self._zha_gateway.devices[ieee].async_remove_from_group( - self.group_id + self._zha_gateway.devices[member.ieee].async_add_endpoint_to_group( + member.endpoint_id, self.group_id ) ) await asyncio.gather(*tasks) else: await self._zha_gateway.devices[ - member_ieee_addresses[0] - ].async_remove_from_group(self.group_id) + members[0].ieee + ].async_add_endpoint_to_group(members[0].endpoint_id, self.group_id) + + async def async_remove_members(self, members: List[GroupMember]) -> None: + """Remove members from this group.""" + if len(members) > 1: + tasks = [] + for member in members: + tasks.append( + self._zha_gateway.devices[ + member.ieee + ].async_remove_endpoint_from_group( + member.endpoint_id, self.group_id + ) + ) + await asyncio.gather(*tasks) + else: + await self._zha_gateway.devices[ + members[0].ieee + ].async_remove_endpoint_from_group(members[0].endpoint_id, self.group_id) @property def member_entity_ids(self) -> List[str]: """Return the ZHA entity ids for all entities for the members of this group.""" all_entity_ids: List[str] = [] - for device in self.members: - entities = async_entries_for_device( - self._zha_gateway.ha_entity_registry, device.device_id - ) - for entity in entities: - all_entity_ids.append(entity.entity_id) + for member in self.members: + entity_references = member.associated_entities + for entity_reference in entity_references: + all_entity_ids.append(entity_reference["entity_id"]) return all_entity_ids def get_domain_entity_ids(self, domain) -> List[str]: """Return entity ids from the entity domain for this group.""" domain_entity_ids: List[str] = [] - for device in self.members: + for member in self.members: entities = async_entries_for_device( - self._zha_gateway.ha_entity_registry, device.device_id + self._zha_gateway.ha_entity_registry, member.device.device_id ) domain_entity_ids.extend( [entity.entity_id for entity in entities if entity.domain == domain] ) return domain_entity_ids - @callback - def async_get_info(self) -> Dict[str, Any]: + @property + def group_info(self) -> Dict[str, Any]: """Get ZHA group info.""" group_info: Dict[str, Any] = {} group_info["group_id"] = self.group_id group_info["name"] = self.name - group_info["members"] = [ - zha_device.async_get_info() for zha_device in self.members - ] + group_info["members"] = [member.member_info for member in self.members] return group_info def log(self, level: int, msg: str, *args): diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 4441ac90717..bb8a202e789 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -101,7 +101,7 @@ def mean_int(*args): def mean_tuple(*args): """Return the mean values along the columns of the supplied values.""" - return tuple(sum(l) / len(l) for l in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args)) def reduce_attribute( diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 29b71343245..6ddf48de5aa 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -3,19 +3,9 @@ import collections from typing import Callable, Dict, List, Set, Tuple, Union import attr -import bellows.ezsp -import bellows.zigbee.application import zigpy.profiles.zha import zigpy.profiles.zll import zigpy.zcl as zcl -import zigpy_cc.api -import zigpy_cc.zigbee.application -import zigpy_deconz.api -import zigpy_deconz.zigbee.application -import zigpy_xbee.api -import zigpy_xbee.zigbee.application -import zigpy_zigate.api -import zigpy_zigate.zigbee.application from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.cover import DOMAIN as COVER @@ -28,7 +18,6 @@ from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import -from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType @@ -129,34 +118,6 @@ LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() CLIENT_CHANNELS_REGISTRY = DictRegistry() -RADIO_TYPES = { - RadioType.deconz.name: { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz, - CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "Deconz", - }, - RadioType.ezsp.name: { - ZHA_GW_RADIO: bellows.ezsp.EZSP, - CONTROLLER: bellows.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "EZSP", - }, - RadioType.ti_cc.name: { - ZHA_GW_RADIO: zigpy_cc.api.API, - CONTROLLER: zigpy_cc.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "TI CC", - }, - RadioType.xbee.name: { - ZHA_GW_RADIO: zigpy_xbee.api.XBee, - CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "XBee", - }, - RadioType.zigate.name: { - ZHA_GW_RADIO: zigpy_zigate.api.ZiGate, - CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "ZiGate", - }, -} - COMPONENT_CLUSTERS = { BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 571741da7c3..0feaf14b3c5 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -5,7 +5,7 @@ import logging from zigpy.zcl.foundation import Status -from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice +from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -42,7 +42,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @STRICT_MATCH(channel_names=CHANNEL_COVER) -class ZhaCover(ZhaEntity, CoverDevice): +class ZhaCover(ZhaEntity, CoverEntity): """Representation of a ZHA cover.""" def __init__(self, unique_id, zha_device, channels, **kwargs): diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index b05e4b7bee0..efe95ae6604 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -97,7 +97,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class BaseLight(LogMixin, light.Light): +class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" def __init__(self, *args, **kwargs): diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index ba802120044..d70c1e2e7f3 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -8,7 +8,7 @@ from homeassistant.components.lock import ( DOMAIN, STATE_LOCKED, STATE_UNLOCKED, - LockDevice, + LockEntity, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @STRICT_MATCH(channel_names=CHANNEL_DOORLOCK) -class ZhaDoorLock(ZhaEntity, LockDevice): +class ZhaDoorLock(ZhaEntity, LockEntity): """Representation of a ZHA lock.""" def __init__(self, unique_id, zha_device, channels, **kwargs): diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e49f4f1407a..ef96d6efef1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,13 +4,14 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.15.2", - "zha-quirks==0.0.38", - "zigpy-cc==0.3.1", - "zigpy-deconz==0.8.1", - "zigpy-homeassistant==0.19.0", - "zigpy-xbee-homeassistant==0.11.0", - "zigpy-zigate==0.5.1" + "bellows==0.16.2", + "pyserial==3.4", + "zha-quirks==0.0.39", + "zigpy-cc==0.4.2", + "zigpy-deconz==0.9.2", + "zigpy==0.20.4", + "zigpy-xbee==0.12.1", + "zigpy-zigate==0.6.1" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 755ba7ae710..b26cebbd40a 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -3,7 +3,22 @@ "step": { "user": { "title": "ZHA", - "data": { "radio_type": "Radio Type", "usb_path": "USB Device Path" } + "data": { "path": "Serial Device Path" }, + "description": "Select serial port for Zigbee radio" + }, + "pick_radio": { + "data": { "radio_type": "Radio Type" }, + "title": "Radio Type", + "description": "Pick a type of your Zigbee radio" + }, + "port_config": { + "title": "Settings", + "description": "Enter port specific settings", + "data": { + "path": "Serial device path", + "baudrate": "port speed", + "flow_control": "data flow control" + } } }, "error": { "cannot_connect": "Unable to connect to ZHA device." }, diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 328d9959ad2..9a7fc7aa6b0 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -6,7 +6,7 @@ from typing import Any, List from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -41,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class BaseSwitch(SwitchDevice): +class BaseSwitch(SwitchEntity): """Common base class for zha switches.""" def __init__(self, *args, **kwargs): diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index ec7bad9997b..a84ef9b4abd 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e", - "usb_path": "\u041f\u044a\u0442 \u0434\u043e USB \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 7102410aed4..d4e8b7997b0 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -7,11 +7,29 @@ "cannot_connect": "No s'ha pogut connectar amb el dispositiu ZHA." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Tipus de r\u00e0dio" + }, + "description": "Tria el teu tipus de r\u00e0dio Zigbee", + "title": "Tipus de r\u00e0dio" + }, + "port_config": { + "data": { + "baudrate": "velocitat del port", + "flow_control": "control de flux de dades", + "path": "Ruta del port s\u00e8rie al dispositiu" + }, + "description": "Introdueix la configuraci\u00f3 espec\u00edfica de port", + "title": "Configuraci\u00f3" + }, "user": { "data": { + "path": "Ruta del port s\u00e8rie al dispositiu", "radio_type": "Tipus de r\u00e0dio", - "usb_path": "Ruta del port USB al dispositiu" + "usb_path": "[%key::common::config_flow::data::usb_path%]" }, + "description": "Selecciona el port s\u00e8rie per a la r\u00e0dio Zigbee", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/da.json b/homeassistant/components/zha/translations/da.json index 43477e1189e..2ebf02c5455 100644 --- a/homeassistant/components/zha/translations/da.json +++ b/homeassistant/components/zha/translations/da.json @@ -7,11 +7,29 @@ "cannot_connect": "Kunne ikke oprette forbindelse til ZHA-enhed." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Radiotype" + }, + "description": "V\u00e6lg en type Zigbee-radio", + "title": "Radiotype" + }, + "port_config": { + "data": { + "baudrate": "porthastighed", + "flow_control": "dataflowstyring", + "path": "Sti til seriel enhed" + }, + "description": "Angiv portspecifikke indstillinger", + "title": "Indstillinger" + }, "user": { "data": { + "path": "Stien til seriel enhed", "radio_type": "Radio-type", - "usb_path": "Sti til USB-enhed" + "usb_path": "USB-enheds sti" }, + "description": "V\u00e6lg seriel port til Zigbee-radio", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index c5ef6071dd7..d25894e338d 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -7,11 +7,29 @@ "cannot_connect": "Kein Verbindung zu ZHA-Ger\u00e4t m\u00f6glich" }, "step": { + "pick_radio": { + "data": { + "radio_type": "Funktyp" + }, + "description": "W\u00e4hlen Sie einen Typ Ihres Zigbee-Funks", + "title": "Funktyp" + }, + "port_config": { + "data": { + "baudrate": "Port-Geschwindigkeit", + "flow_control": "Datenflusskontrolle", + "path": "Serieller Ger\u00e4tepfad" + }, + "description": "Geben Sie die portspezifischen Einstellungen ein", + "title": "Einstellungen" + }, "user": { "data": { + "path": "Serieller Ger\u00e4tepfad", "radio_type": "Radio-Type", "usb_path": "USB-Ger\u00e4te-Pfad" }, + "description": "W\u00e4hlen Sie die serielle Schnittstelle f\u00fcr den ZigBee-Funk", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index d8db817507d..760492552a7 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -7,11 +7,29 @@ "cannot_connect": "Unable to connect to ZHA device." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Radio Type" + }, + "description": "Pick a type of your Zigbee radio", + "title": "Radio Type" + }, + "port_config": { + "data": { + "baudrate": "port speed", + "flow_control": "data flow control", + "path": "Serial device path" + }, + "description": "Enter port specific settings", + "title": "Settings" + }, "user": { "data": { + "path": "Serial Device Path", "radio_type": "Radio Type", "usb_path": "USB Device Path" }, + "description": "Select serial port for Zigbee radio", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/es-419.json b/homeassistant/components/zha/translations/es-419.json index 81803aa8cf4..36dadce7087 100644 --- a/homeassistant/components/zha/translations/es-419.json +++ b/homeassistant/components/zha/translations/es-419.json @@ -9,15 +9,35 @@ "step": { "user": { "data": { - "radio_type": "Tipo de radio", - "usb_path": "Ruta del dispositivo USB" + "radio_type": "Tipo de radio" }, "title": "ZHA" } } }, "device_automation": { + "action_type": { + "squawk": "Graznido", + "warn": "Advertir" + }, "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Aumentar intensidad", + "face_1": "con la cara 1 activada", + "face_2": "con la cara 2 activada", + "face_3": "con la cara 3 activada", + "face_4": "con la cara 4 activada", + "face_5": "con la cara 5 activada", + "face_6": "con la cara 6 activada", + "face_any": "Con cualquier cara/especificada(s) activada(s)", "left": "Izquierda", "open": "Abrir", "right": "Derecha", @@ -31,7 +51,23 @@ "device_rotated": "Dispositivo girado \"{subtype}\"", "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \"{subtype}\"", - "device_tilted": "Dispositivo inclinado" + "device_tilted": "Dispositivo inclinado", + "remote_button_alt_double_press": "El bot\u00f3n \"{subtype}\" fue presionado 2 veces (modo alternativo)", + "remote_button_alt_long_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado continuamente (modo alternativo)", + "remote_button_alt_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada (modo alternativo)", + "remote_button_alt_quadruple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 4 veces (modo alternativo)", + "remote_button_alt_quintuple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 5 veces (modo alternativo)", + "remote_button_alt_short_press": "El bot\u00f3n \"{subtype}\" ha sido presionado (modo alternativo)", + "remote_button_alt_short_release": "El bot\u00f3n \"{subtype}\" ha sido soltado (modo alternativo)", + "remote_button_alt_triple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 3 veces (modo alternativo)", + "remote_button_double_press": "El bot\u00f3n \"{subtype}\" fue presionado 2 veces", + "remote_button_long_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado continuamente", + "remote_button_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 4 veces", + "remote_button_quintuple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 5 veces", + "remote_button_short_press": "Se presion\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_short_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_triple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 3 veces" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 19767b7662f..97bbc5ce033 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -7,11 +7,29 @@ "cannot_connect": "No se puede conectar al dispositivo ZHA." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Tipo de Radio" + }, + "description": "Selecciona el tipo de tu radio Zigbee", + "title": "Tipo de Radio" + }, + "port_config": { + "data": { + "baudrate": "velocidad del puerto", + "flow_control": "control de flujo de datos", + "path": "Ruta del dispositivo serie" + }, + "description": "Introduce los ajustes espec\u00edficos del puerto", + "title": "Configuraci\u00f3n" + }, "user": { "data": { + "path": "Ruta del Dispositivo Serie", "radio_type": "Tipo de radio", - "usb_path": "Ruta del dispositivo USB" + "usb_path": "Ruta del Dispositivo USB" }, + "description": "Selecciona puerto serie para radio Zigbee", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/fi.json b/homeassistant/components/zha/translations/fi.json new file mode 100644 index 00000000000..5e008b35ddf --- /dev/null +++ b/homeassistant/components/zha/translations/fi.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "cannot_connect": "Yhteyden muodostaminen ZHA-laitteeseen ei onnistu." + }, + "step": { + "pick_radio": { + "data": { + "radio_type": "Radiotyyppi" + }, + "description": "Valitse Zigbee-radion tyyppi", + "title": "Radion tyyppi" + }, + "port_config": { + "data": { + "baudrate": "portin nopeus", + "flow_control": "tietovirran hallinta", + "path": "Sarjalaitteen polku" + }, + "description": "Anna porttikohtaiset asetukset", + "title": "Asetukset" + }, + "user": { + "data": { + "radio_type": "Radiotyyppi" + }, + "title": "ZHA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index d3ca58546f1..035ca744dda 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -7,10 +7,22 @@ "cannot_connect": "Impossible de se connecter au p\u00e9riph\u00e9rique ZHA." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Type de radio" + }, + "description": "Choisissez un type de radio Zigbee", + "title": "Type de radio" + }, + "port_config": { + "data": { + "baudrate": "vitesse du port" + }, + "title": "R\u00e9glages" + }, "user": { "data": { - "radio_type": "Type de radio", - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" + "radio_type": "Type de radio" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json new file mode 100644 index 00000000000..2ede9ae4430 --- /dev/null +++ b/homeassistant/components/zha/translations/he.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "port_config": { + "title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 0f1d3985923..4f9e9796925 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -7,10 +7,15 @@ "cannot_connect": "Nem lehet csatlakozni a ZHA eszk\u00f6zh\u00f6z." }, "step": { + "port_config": { + "data": { + "baudrate": "port sebess\u00e9g" + }, + "title": "Be\u00e1ll\u00edt\u00e1sok" + }, "user": { "data": { - "radio_type": "R\u00e1di\u00f3 t\u00edpusa", - "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + "radio_type": "R\u00e1di\u00f3 t\u00edpusa" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 05d429701f0..be5da92433c 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -7,11 +7,29 @@ "cannot_connect": "Impossibile connettersi al dispositivo ZHA." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Tipo di radio" + }, + "description": "Scegli un tipo di radio Zigbee", + "title": "Tipo di radio" + }, + "port_config": { + "data": { + "baudrate": "velocit\u00e0 della porta", + "flow_control": "controllo del flusso di dati", + "path": "Percorso del dispositivo seriale" + }, + "description": "Inserire le impostazioni specifiche della porta", + "title": "Impostazioni" + }, "user": { "data": { + "path": "Percorso del dispositivo seriale", "radio_type": "Tipo di Radio", "usb_path": "Percorso del dispositivo USB" }, + "description": "Selezionare la porta seriale per la radio Zigbee", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json index 99438e10a84..f1190f590d4 100644 --- a/homeassistant/components/zha/translations/ko.json +++ b/homeassistant/components/zha/translations/ko.json @@ -7,11 +7,29 @@ "cannot_connect": "ZHA \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { + "pick_radio": { + "data": { + "radio_type": "\ubb34\uc120 \uc720\ud615" + }, + "description": "\uc9c0\uadf8\ube44 \ubb34\uc120 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\ubb34\uc120 \uc720\ud615" + }, + "port_config": { + "data": { + "baudrate": "\ud3ec\ud2b8 \uc18d\ub3c4", + "flow_control": "\ub370\uc774\ud130 \ud750\ub984 \uc81c\uc5b4", + "path": "\uc2dc\ub9ac\uc5bc \uc7a5\uce58 \uacbd\ub85c" + }, + "description": "\uac01 \ud3ec\ud2b8\ubcc4 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "\uc124\uc815\ub0b4\uc6a9" + }, "user": { "data": { + "path": "\uc2dc\ub9ac\uc5bc \uc7a5\uce58 \uacbd\ub85c", "radio_type": "\ubb34\uc120 \uc720\ud615", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" }, + "description": "Zigbee \ubb34\uc120 \uc7a5\uce58\uc758 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/lb.json b/homeassistant/components/zha/translations/lb.json index 3c84a7e6d5d..5f72f64b099 100644 --- a/homeassistant/components/zha/translations/lb.json +++ b/homeassistant/components/zha/translations/lb.json @@ -7,11 +7,29 @@ "cannot_connect": "Keng Verbindung mam ZHA Apparat m\u00e9iglech." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Typ vun Radio" + }, + "description": "Typ vum Zigbee Radio auswielen", + "title": "Typ vun Radio" + }, + "port_config": { + "data": { + "baudrate": "Vitesse vum Port", + "flow_control": "Data Flow Kontroll", + "path": "Pad zum seriellen Apparat" + }, + "description": "G\u00ebff spezifesch Port Astellungen an.", + "title": "Astellungen" + }, "user": { "data": { + "path": "Pad zum seriellen Apparat", "radio_type": "Typ vun Radio", "usb_path": "Pad zum USB Apparat" }, + "description": "Serielle Port fir Zigbee Radio auswielen", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index f3eb4009f02..e06c186cbef 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "radio_type": "Radio Type", - "usb_path": "USB-apparaatpad" + "radio_type": "Radio Type" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index c36bd66304b..fe5714d5359 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -7,18 +7,36 @@ "cannot_connect": "Kan ikke koble til ZHA-enhet." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Radio type" + }, + "description": "Velg din type Zigbee-radio", + "title": "Radio type" + }, + "port_config": { + "data": { + "baudrate": "porthastighet", + "flow_control": "data flytkontroll", + "path": "Seriell enhetsbane" + }, + "description": "Angi portspesifikke innstillinger", + "title": "Innstillinger" + }, "user": { "data": { + "path": "Seriell enhetsbane", "radio_type": "Radio type", "usb_path": "USB enhetsbane" }, + "description": "Velg seriell port for Zigbee radio", "title": "" } } }, "device_automation": { "action_type": { - "squawk": "Varsle", + "squawk": "Squawk", "warn": "Advar" }, "trigger_subtype": { @@ -46,29 +64,29 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "device_dropped": "Enheten ble sluppet", + "device_dropped": "Enhet droppet", "device_flipped": "Enheten snudd \"{subtype}\"", "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enheten skr\u00e5stilt", - "remote_button_alt_double_press": "\" {subtype} \" -knapp dobbeltklikket (alternativ modus)", - "remote_button_alt_long_press": "\" {subtype} \" -knappen trykkes kontinuerlig (alternativ modus)", - "remote_button_alt_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk (Alternativ modus)", - "remote_button_alt_quadruple_press": "\"{subtype}\" knapp firedoblet klikket (alternativ modus)", - "remote_button_alt_quintuple_press": "\"{subtype}\" knapp femdobblet klikket (alternativ modus)", - "remote_button_alt_short_press": "\" {subtype} \" -knappen trykket p\u00e5 (alternativ modus)", - "remote_button_alt_short_release": "\" {subtype} \" -knapp utgitt (alternativ modus)", - "remote_button_alt_triple_press": "\" {subtype} \" -knapp tredobbeltklikket (alternativ modus)", - "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{subtype}\"-knappen ble holdt inne", - "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knappen ble trykket fire ganger", - "remote_button_quintuple_press": "\"{subtype}\"-knappen ble trykket fem ganger", - "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\"{subtype}\"-knappen ble trippelklikket" + "remote_button_alt_double_press": "\"{subtype}\"-knapp trykket p\u00e5 to ganger (vekslende modus)", + "remote_button_alt_long_press": "\"{subtype}\"-knapp holdt inne (vekslende modus)", + "remote_button_alt_long_release": "\"{subtype}\"-knapp sluppet etter langt trykk (vekslende modus)", + "remote_button_alt_quadruple_press": "\"{subtype}\"-knapp trykket p\u00e5 fire ganger (vekslende modus)", + "remote_button_alt_quintuple_press": "\"{subtype}\"-knapp trykket p\u00e5 fem ganger (vekslende modus)", + "remote_button_alt_short_press": "\"{subtype}\"-knapp trykket p\u00e5 (vekslende modus)", + "remote_button_alt_short_release": "\"{subtype}\"-knapp sluppet (vekslende modus)", + "remote_button_alt_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger (vekslende modus)", + "remote_button_double_press": "\"{subtype}\" knapp trykket p\u00e5 to ganger", + "remote_button_long_press": "\"{subtype}\"-knapp holdt inne", + "remote_button_long_release": "\"{subtype}\"-knapp sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knapp trykket p\u00e5 fire ganger", + "remote_button_quintuple_press": "\"{subtype}\"-knapp trykket p\u00e5 fem ganger", + "remote_button_short_press": "\"{subtype}\"-knapp trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knapp sluppet", + "remote_button_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 164d1b1a730..438cd44d5b5 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -7,11 +7,29 @@ "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem ZHA." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Typ radia" + }, + "description": "Wyb\u00f3r typu radia Zigbee", + "title": "Typ radia" + }, + "port_config": { + "data": { + "baudrate": "pr\u0119dko\u015b\u0107 portu", + "flow_control": "kontrola przep\u0142ywu danych", + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" + }, + "description": "Wprowadzanie ustawie\u0144 dla portu", + "title": "Ustawienia" + }, "user": { "data": { + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego", "radio_type": "Typ radia", - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + "usb_path": "[%key_id:common::config_flow::data::usb_path%]" }, + "description": "Wyb\u00f3r portu szeregowego dla radia Zigbee", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 60d9f681be0..8a9b2b21677 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "radio_type": "Tipo de r\u00e1dio", - "usb_path": "Caminho do Dispositivo USB" + "radio_type": "Tipo de r\u00e1dio" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 2c810af8eae..1b9073b21f0 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "radio_type": "Tipo de r\u00e1dio", - "usb_path": "Caminho do Dispositivo USB" + "radio_type": "Tipo de r\u00e1dio" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index d05dff4e478..557b17ebe92 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -7,11 +7,29 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, "step": { + "pick_radio": { + "data": { + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "port_config": { + "data": { + "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0430", + "flow_control": "\u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0445", + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0440\u0442\u0430", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, "user": { "data": { + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443", "radio_type": "\u0422\u0438\u043f \u0420\u0430\u0434\u0438\u043e", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430 \u0441\u0435\u0442\u0438 Zigbee", "title": "Zigbee Home Automation" } } diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 84ae9155b37..00358fb1e23 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -7,10 +7,26 @@ "cannot_connect": "Det gick inte att ansluta till ZHA enhet." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Radiotyp" + }, + "description": "V\u00e4lj en typ av Zigbee radio", + "title": "Radiotyp" + }, + "port_config": { + "data": { + "baudrate": "port hastighet", + "flow_control": "datafl\u00f6deskontroll", + "path": "Seriell enhetsv\u00e4g" + }, + "description": "Ange portspecifika inst\u00e4llningar", + "title": "Inst\u00e4llningar" + }, "user": { "data": { - "radio_type": "Typ av radio", - "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + "path": "Seriell enhetsv\u00e4g", + "radio_type": "Typ av radio" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index 72756d78a65..d5c25573b7b 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "radio_type": "\u65e0\u7ebf\u7535\u7c7b\u578b", - "usb_path": "USB \u8bbe\u5907\u8def\u5f84" + "radio_type": "\u65e0\u7ebf\u7535\u7c7b\u578b" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 6171f68a081..ecfd8ff9e51 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -7,11 +7,29 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u8a2d\u5099\u3002" }, "step": { + "pick_radio": { + "data": { + "radio_type": "\u7121\u7dda\u96fb\u985e\u578b" + }, + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u578b", + "title": "\u7121\u7dda\u96fb\u985e\u578b" + }, + "port_config": { + "data": { + "baudrate": "\u901a\u8a0a\u57e0\u901f\u5ea6", + "flow_control": "\u8cc7\u6599\u6d41\u91cf\u63a7\u5236", + "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91" + }, + "description": "\u8f38\u5165\u901a\u8a0a\u57e0\u7279\u5b9a\u8a2d\u5b9a", + "title": "\u8a2d\u5b9a" + }, "user": { "data": { + "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91", "radio_type": "\u7121\u7dda\u96fb\u985e\u578b", "usb_path": "USB \u8a2d\u5099\u8def\u5f91" }, + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", "title": "ZHA" } } diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 62f5b9acbaf..fc5a5688027 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -5,7 +5,7 @@ import voluptuous as vol from zhong_hong_hvac.hub import ZhongHongGateway from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, HVAC_MODE_COOL, @@ -113,7 +113,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_listen) -class ZhongHongClimate(ClimateDevice): +class ZhongHongClimate(ClimateEntity): """Representation of a ZhongHong controller support HVAC.""" def __init__(self, hub, addr_out, addr_in): diff --git a/homeassistant/components/zigbee/binary_sensor.py b/homeassistant/components/zigbee/binary_sensor.py index d32554e5744..fe35b54a88f 100644 --- a/homeassistant/components/zigbee/binary_sensor.py +++ b/homeassistant/components/zigbee/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Zigbee binary sensors.""" import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DOMAIN, PLATFORM_SCHEMA, ZigBeeDigitalIn, ZigBeeDigitalInConfig @@ -21,5 +21,5 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): +class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorEntity): """Use ZigBeeDigitalIn as binary sensor.""" diff --git a/homeassistant/components/zigbee/light.py b/homeassistant/components/zigbee/light.py index 54f6044c3dd..10bb87aa426 100644 --- a/homeassistant/components/zigbee/light.py +++ b/homeassistant/components/zigbee/light.py @@ -1,7 +1,7 @@ """Support for Zigbee lights.""" import voluptuous as vol -from homeassistant.components.light import Light +from homeassistant.components.light import LightEntity from . import DOMAIN, PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig @@ -21,5 +21,5 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ZigBeeLight(ZigBeeDigitalOutConfig(config), zigbee_device)]) -class ZigBeeLight(ZigBeeDigitalOut, Light): +class ZigBeeLight(ZigBeeDigitalOut, LightEntity): """Use ZigBeeDigitalOut as light.""" diff --git a/homeassistant/components/zigbee/switch.py b/homeassistant/components/zigbee/switch.py index e29d2c045df..f5b73f5d328 100644 --- a/homeassistant/components/zigbee/switch.py +++ b/homeassistant/components/zigbee/switch.py @@ -1,7 +1,7 @@ """Support for Zigbee switches.""" import voluptuous as vol -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from . import DOMAIN, PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig @@ -20,5 +20,5 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ZigBeeSwitch(ZigBeeDigitalOutConfig(config), zigbee_device)]) -class ZigBeeSwitch(ZigBeeDigitalOut, SwitchDevice): +class ZigBeeSwitch(ZigBeeDigitalOut, SwitchEntity): """Representation of a Zigbee Digital Out device.""" diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index 832758e26fb..d3493f0dc35 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -5,7 +5,7 @@ import socket import voluptuous as vol from ziggo_mediabox_xl import ZiggoMediaboxXL -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -92,7 +92,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(hosts, True) -class ZiggoMediaboxXLDevice(MediaPlayerDevice): +class ZiggoMediaboxXLDevice(MediaPlayerEntity): """Representation of a Ziggo Mediabox XL Device.""" def __init__(self, mediabox, host, name, available): diff --git a/homeassistant/components/zone/translations/fi.json b/homeassistant/components/zone/translations/fi.json new file mode 100644 index 00000000000..5530d246958 --- /dev/null +++ b/homeassistant/components/zone/translations/fi.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nimi on jo olemassa" + }, + "step": { + "init": { + "data": { + "icon": "Kuvake", + "latitude": "Leveysaste", + "longitude": "Pituusaste", + "name": "Nimi", + "passive": "Passiivinen", + "radius": "S\u00e4de" + }, + "title": "M\u00e4\u00e4rit\u00e4 vy\u00f6hykeparametrit" + } + }, + "title": "Vy\u00f6hyke" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/ko.json b/homeassistant/components/zone/translations/ko.json index 421f079a67e..789ec882dc0 100644 --- a/homeassistant/components/zone/translations/ko.json +++ b/homeassistant/components/zone/translations/ko.json @@ -13,7 +13,7 @@ "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", "radius": "\ubc18\uacbd" }, - "title": "\uad6c\uc5ed \uc124\uc815" + "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758\ud558\uae30" } }, "title": "\uad6c\uc5ed" diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 16c22cb48e7..739864fdea8 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,5 +1,5 @@ """Support for ZoneMinder binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from . import DOMAIN as ZONEMINDER_DOMAIN @@ -13,7 +13,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): return True -class ZMAvailabilitySensor(BinarySensorDevice): +class ZMAvailabilitySensor(BinarySensorEntity): """Representation of the availability of ZoneMinder as a binary sensor.""" def __init__(self, host_name, client): diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 5eaf2ed4901..0428ddbf888 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from zoneminder.monitor import MonitorState -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON import homeassistant.helpers.config_validation as cv @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(switches) -class ZMSwitchMonitors(SwitchDevice): +class ZMSwitchMonitors(SwitchEntity): """Representation of a ZoneMinder switch.""" icon = "mdi:record-rec" diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index e4bafc44bee..094279c4e7a 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -2,7 +2,7 @@ import datetime import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import track_point_in_time @@ -39,7 +39,7 @@ def get_device(values, **kwargs): return None -class ZWaveBinarySensor(BinarySensorDevice, ZWaveDeviceEntity): +class ZWaveBinarySensor(BinarySensorEntity, ZWaveDeviceEntity): """Representation of a binary sensor within Z-Wave.""" def __init__(self, values, device_class): diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 4ee9b8b9cc9..9c9c1ed6128 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -3,7 +3,7 @@ import logging from typing import Optional, Tuple -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -149,7 +149,7 @@ def get_device(hass, values, **kwargs): return None -class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): +class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): """Representation of a Z-Wave Climate device.""" def __init__(self, values, temp_unit): diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index e6aa8028849..688ee666676 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -6,7 +6,7 @@ from homeassistant.components.cover import ( DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, - CoverDevice, + CoverEntity, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -56,7 +56,7 @@ def get_device(hass, values, node_config, **kwargs): return None -class ZwaveRollershutter(ZWaveDeviceEntity, CoverDevice): +class ZwaveRollershutter(ZWaveDeviceEntity, CoverEntity): """Representation of an Z-Wave cover.""" def __init__(self, hass, values, invert_buttons, invert_percent): @@ -140,7 +140,7 @@ class ZwaveRollershutter(ZWaveDeviceEntity, CoverDevice): self._network.manager.releaseButton(self._open_id) -class ZwaveGarageDoorBase(ZWaveDeviceEntity, CoverDevice): +class ZwaveGarageDoorBase(ZWaveDeviceEntity, CoverEntity): """Base class for a Zwave garage door device.""" def __init__(self, values): diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 5e4b83d81e1..f8674a48a32 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -56,6 +56,7 @@ DISCOVERY_SCHEMAS = [ const.DISC_SPECIFIC_DEVICE_CLASS: [ const.SPECIFIC_TYPE_THERMOSTAT_HEATING, const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + const.SPECIFIC_TYPE_NOT_USED, ], const.DISC_VALUES: dict( DEFAULT_VALUES_SCHEMA, diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 745400e5c44..1856aeb9623 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -14,7 +14,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - Light, + LightEntity, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback @@ -118,7 +118,7 @@ def ct_to_hs(temp): return [int(val) for val in colorlist] -class ZwaveDimmer(ZWaveDeviceEntity, Light): +class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): """Representation of a Z-Wave dimmer.""" def __init__(self, values, refresh, delay): diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index 0bbcf9815c6..c9601679f57 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.lock import DOMAIN, LockDevice +from homeassistant.components.lock import DOMAIN, LockEntity from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -38,6 +38,10 @@ DEVICE_MAPPINGS = { (0x0090, 0x238): WORKAROUND_DEVICE_STATE, # Kwikset 888ZW500-15S Smartcode 888 (0x0090, 0x541): WORKAROUND_DEVICE_STATE, + # Kwikset 916 + (0x0090, 0x0001): WORKAROUND_DEVICE_STATE, + # Kwikset Obsidian + (0x0090, 0x0742): WORKAROUND_DEVICE_STATE, # Yale Locks # Yale YRD210, YRD220, YRL220 (0x0129, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, @@ -239,7 +243,7 @@ def get_device(node, values, **kwargs): return ZwaveLock(values) -class ZwaveLock(ZWaveDeviceEntity, LockDevice): +class ZwaveLock(ZWaveDeviceEntity, LockEntity): """Representation of a Z-Wave Lock.""" def __init__(self, values): diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json index 3c62a89dc25..cab8e7461cb 100644 --- a/homeassistant/components/zwave/strings.json +++ b/homeassistant/components/zwave/strings.json @@ -5,7 +5,7 @@ "title": "Set up Z-Wave", "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", "data": { - "usb_path": "USB Path", + "usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key (leave blank to auto-generate)" } } @@ -30,4 +30,4 @@ "ready": "Ready" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py index 4956e99a40e..c5770bf7702 100644 --- a/homeassistant/components/zwave/switch.py +++ b/homeassistant/components/zwave/switch.py @@ -2,7 +2,7 @@ import logging import time -from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,7 +27,7 @@ def get_device(values, **kwargs): return ZwaveSwitch(values) -class ZwaveSwitch(ZWaveDeviceEntity, SwitchDevice): +class ZwaveSwitch(ZWaveDeviceEntity, SwitchEntity): """Representation of a Z-Wave switch.""" def __init__(self, values): diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json index f277e1e4c68..bd27966b6a5 100644 --- a/homeassistant/components/zwave/translations/en.json +++ b/homeassistant/components/zwave/translations/en.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "Network Key (leave blank to auto-generate)", - "usb_path": "USB Path" + "usb_path": "USB Device Path" }, "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", "title": "Set up Z-Wave" diff --git a/homeassistant/components/zwave/translations/fi.json b/homeassistant/components/zwave/translations/fi.json index fde97739c63..5cddea71d32 100644 --- a/homeassistant/components/zwave/translations/fi.json +++ b/homeassistant/components/zwave/translations/fi.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "usb_path": "USB-polku" + }, + "title": "Z-Waven m\u00e4\u00e4ritt\u00e4minen" + } + } + }, "state": { "_": { "dead": "Kuollut", diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index e8e3b78a25e..8b8ffb732fc 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -21,13 +21,13 @@ "state": { "_": { "dead": "Disattivo", - "initializing": "Avvio", + "initializing": "In avvio", "ready": "Pronto", - "sleeping": "In attesa" + "sleeping": "Dormiente" }, "query_stage": { - "dead": "Disattivo ({query_stage})", - "initializing": "Avvio ({query_stage})" + "dead": "Disattivo", + "initializing": "In avvio" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json index a30e2b40b9f..0cf4e05c19d 100644 --- a/homeassistant/components/zwave/translations/ko.json +++ b/homeassistant/components/zwave/translations/ko.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", - "usb_path": "USB \uacbd\ub85c" + "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" }, "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/docs/z-wave/installation/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Z-Wave \uc124\uc815" @@ -26,8 +26,8 @@ "sleeping": "\uc808\uc804\ubaa8\ub4dc" }, "query_stage": { - "dead": "\uc751\ub2f5\uc5c6\uc74c ({query_stage})", - "initializing": "\ucd08\uae30\ud654\uc911 ({query_stage})" + "dead": "\uc751\ub2f5\uc5c6\uc74c", + "initializing": "\ucd08\uae30\ud654\uc911" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json index dc3513a3c71..96e697c021a 100644 --- a/homeassistant/components/zwave/translations/nl.json +++ b/homeassistant/components/zwave/translations/nl.json @@ -26,8 +26,8 @@ "sleeping": "Slaapt" }, "query_stage": { - "dead": "Onbereikbaar ({query_stage})", - "initializing": "Initialiseren ( {query_stage} )" + "dead": "Onbereikbaar", + "initializing": "Initialiseren" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json index 1a214262feb..d2614bbb2c7 100644 --- a/homeassistant/components/zwave/translations/no.json +++ b/homeassistant/components/zwave/translations/no.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk generering)", + "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)", "usb_path": "USB bane" }, "description": "Se [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjon variablene", @@ -24,6 +24,10 @@ "initializing": "Initialiserer", "ready": "Klar", "sleeping": "Sover" + }, + "query_stage": { + "dead": "D\u00f8d", + "initializing": "Initialiserer" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sl.json b/homeassistant/components/zwave/translations/sl.json index 5ea01ecbb3b..ccd3c5dfdd6 100644 --- a/homeassistant/components/zwave/translations/sl.json +++ b/homeassistant/components/zwave/translations/sl.json @@ -20,14 +20,14 @@ }, "state": { "_": { - "dead": "Mrtev", + "dead": "Mrtva", "initializing": "Inicializacija", "ready": "Pripravljen", - "sleeping": "Spanje" + "sleeping": "Spi" }, "query_stage": { - "dead": "Mrtev ({query_stage})", - "initializing": "Inicializacija ({query_stage})" + "dead": "Mrtva", + "initializing": "Inicializacija" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index b8e8f00177a..6742964e87a 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "usb_path": "USB \u8def\u5f91" + "usb_path": "USB \u8a2d\u5099\u8def\u5f91" }, "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/", "title": "\u8a2d\u5b9a Z-Wave" diff --git a/homeassistant/config.py b/homeassistant/config.py index 56bbe76a045..dd5e16f42de 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,5 +1,4 @@ """Module to help with parsing and generating configuration files.""" -# pylint: disable=no-name-in-module from collections import OrderedDict from distutils.version import LooseVersion # pylint: disable=import-error import logging @@ -27,7 +26,9 @@ from homeassistant.const import ( CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, CONF_ELEVATION, + CONF_EXTERNAL_URL, CONF_ID, + CONF_INTERNAL_URL, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, @@ -74,10 +75,6 @@ DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) default_config: -# Uncomment this if you are using SSL/TLS, running in Docker container, etc. -# http: -# base_url: example.duckdns.org:8123 - # Text to speech tts: - platform: google_translate @@ -183,9 +180,11 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, CONF_UNIT_SYSTEM: cv.unit_system, CONF_TIME_ZONE: cv.time_zone, - vol.Optional(CONF_WHITELIST_EXTERNAL_DIRS): - # pylint: disable=no-value-for-parameter - vol.All(cv.ensure_list, [vol.IsDir()]), + vol.Optional(CONF_INTERNAL_URL): cv.url, + vol.Optional(CONF_EXTERNAL_URL): cv.url, + vol.Optional(CONF_WHITELIST_EXTERNAL_DIRS): vol.All( + cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + ), vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, vol.Optional(CONF_AUTH_PROVIDERS): vol.All( cv.ensure_list, @@ -478,6 +477,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non CONF_ELEVATION, CONF_TIME_ZONE, CONF_UNIT_SYSTEM, + CONF_EXTERNAL_URL, + CONF_INTERNAL_URL, ] ): hac.config_source = SOURCE_YAML @@ -487,6 +488,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non (CONF_LONGITUDE, "longitude"), (CONF_NAME, "location_name"), (CONF_ELEVATION, "elevation"), + (CONF_INTERNAL_URL, "internal_url"), + (CONF_EXTERNAL_URL, "external_url"), ): if key in config: setattr(hac, attr, config[key]) @@ -529,10 +532,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non hac.units = METRIC_SYSTEM elif CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] - if unit == TEMP_CELSIUS: - hac.units = METRIC_SYSTEM - else: - hac.units = IMPERIAL_SYSTEM + hac.units = METRIC_SYSTEM if unit == TEMP_CELSIUS else IMPERIAL_SYSTEM _LOGGER.warning( "Found deprecated temperature unit in core " "configuration expected unit system. Replace '%s: %s' " diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d4f76d9bb37..6dc259d6515 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -22,6 +22,7 @@ _UNDEF: dict = {} SOURCE_DISCOVERY = "discovery" SOURCE_IMPORT = "import" +SOURCE_INTEGRATION_DISCOVERY = "integration_discovery" SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" @@ -851,8 +852,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): if progress["context"].get("unique_id") == unique_id: raise data_entry_flow.AbortFlow("already_in_progress") - # pylint: disable=no-member - self.context["unique_id"] = unique_id + self.context["unique_id"] = unique_id # pylint: disable=no-member for entry in self._async_current_entries(): if entry.unique_id == unique_id: diff --git a/homeassistant/const.py b/homeassistant/const.py index 00b574f730e..e7eddca36e0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 109 -PATCH_VERSION = "6" +MINOR_VERSION = 110 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) @@ -34,6 +34,7 @@ CONF_AFTER = "after" CONF_ALIAS = "alias" CONF_API_KEY = "api_key" CONF_API_VERSION = "api_version" +CONF_ARMING_TIME = "arming_time" CONF_AT = "at" CONF_AUTH_MFA_MODULES = "auth_mfa_modules" CONF_AUTH_PROVIDERS = "auth_providers" @@ -87,6 +88,7 @@ CONF_EVENT = "event" CONF_EVENT_DATA = "event_data" CONF_EVENT_DATA_TEMPLATE = "event_data_template" CONF_EXCLUDE = "exclude" +CONF_EXTERNAL_URL = "external_url" CONF_FILE_PATH = "file_path" CONF_FILENAME = "filename" CONF_FOR = "for" @@ -101,6 +103,7 @@ CONF_ICON = "icon" CONF_ICON_TEMPLATE = "icon_template" CONF_ID = "id" CONF_INCLUDE = "include" +CONF_INTERNAL_URL = "internal_url" CONF_IP_ADDRESS = "ip_address" CONF_LATITUDE = "latitude" CONF_LIGHTS = "lights" @@ -345,6 +348,7 @@ ATTR_TEMPERATURE = "temperature" # #### UNITS OF MEASUREMENT #### # Power units POWER_WATT = "W" +POWER_KILO_WATT = f"k{POWER_WATT}" # Voltage units VOLT = "V" diff --git a/homeassistant/core.py b/homeassistant/core.py index c799656df89..34df648a4df 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -9,6 +9,7 @@ from concurrent.futures import ThreadPoolExecutor import datetime import enum import functools +from ipaddress import ip_address import logging import os import pathlib @@ -29,12 +30,14 @@ from typing import ( Set, TypeVar, Union, + cast, ) import uuid from async_timeout import timeout import attr import voluptuous as vol +import yarl from homeassistant import block_async_io, loader, util from homeassistant.const import ( @@ -68,9 +71,10 @@ from homeassistant.exceptions import ( ServiceNotFound, Unauthorized, ) -from homeassistant.util import location +from homeassistant.util import location, network from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe import homeassistant.util.dt as dt_util +from homeassistant.util.thread import fix_threading_exception_logging from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem # Typing imports that create a circular dependency @@ -80,9 +84,11 @@ if TYPE_CHECKING: block_async_io.enable() +fix_threading_exception_logging() -# pylint: disable=invalid-name T = TypeVar("T") +_UNDEF: dict = {} +# pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) CALLBACK_TYPE = Callable[[], None] # pylint: enable=invalid-name @@ -424,7 +430,7 @@ class HomeAssistant: # regardless of the state of the loop. if self.state == CoreState.not_running: # just ignore return - if self.state == CoreState.stopping or self.state == CoreState.final_write: + if self.state in [CoreState.stopping, CoreState.final_write]: _LOGGER.info("async_stop called twice: ignored") return if self.state == CoreState.starting: @@ -1299,6 +1305,8 @@ class Config: self.location_name: str = "Home" self.time_zone: datetime.tzinfo = dt_util.UTC self.units: UnitSystem = METRIC_SYSTEM + self.internal_url: Optional[str] = None + self.external_url: Optional[str] = None self.config_source: str = "default" @@ -1383,6 +1391,8 @@ class Config: "version": __version__, "config_source": self.config_source, "safe_mode": self.safe_mode, + "external_url": self.external_url, + "internal_url": self.internal_url, } def set_time_zone(self, time_zone_str: str) -> None: @@ -1406,6 +1416,8 @@ class Config: unit_system: Optional[str] = None, location_name: Optional[str] = None, time_zone: Optional[str] = None, + external_url: Optional[Union[str, dict]] = _UNDEF, + internal_url: Optional[Union[str, dict]] = _UNDEF, ) -> None: """Update the configuration from a dictionary.""" self.config_source = source @@ -1424,6 +1436,10 @@ class Config: self.location_name = location_name if time_zone is not None: self.set_time_zone(time_zone) + if external_url is not _UNDEF: + self.external_url = cast(Optional[str], external_url) + if internal_url is not _UNDEF: + self.internal_url = cast(Optional[str], internal_url) async def async_update(self, **kwargs: Any) -> None: """Update the configuration from a dictionary.""" @@ -1437,10 +1453,51 @@ class Config: CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True ) data = await store.async_load() - if not data: - return - self._update(source=SOURCE_STORAGE, **data) + async def migrate_base_url(_: Event) -> None: + """Migrate base_url to internal_url/external_url.""" + if self.hass.config.api is None: + return + + base_url = yarl.URL(self.hass.config.api.deprecated_base_url) + + # Check if this is an internal URL + if str(base_url.host).endswith(".local") or ( + network.is_ip_address(str(base_url.host)) + and network.is_private(ip_address(base_url.host)) + ): + await self.async_update( + internal_url=network.normalize_url(str(base_url)) + ) + return + + # External, ensure this is not a loopback address + if not ( + network.is_ip_address(str(base_url.host)) + and network.is_loopback(ip_address(base_url.host)) + ): + await self.async_update( + external_url=network.normalize_url(str(base_url)) + ) + + if data: + # Try to migrate base_url to internal_url/external_url + if "external_url" not in data: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, migrate_base_url + ) + + self._update( + source=SOURCE_STORAGE, + latitude=data.get("latitude"), + longitude=data.get("longitude"), + elevation=data.get("elevation"), + unit_system=data.get("unit_system"), + location_name=data.get("location_name"), + time_zone=data.get("time_zone"), + external_url=data.get("external_url", _UNDEF), + internal_url=data.get("internal_url", _UNDEF), + ) async def async_store(self) -> None: """Store [homeassistant] core config.""" @@ -1455,6 +1512,8 @@ class Config: "unit_system": self.units.name, "location_name": self.location_name, "time_zone": time_zone, + "external_url": self.external_url, + "internal_url": self.internal_url, } store = self.hass.helpers.storage.Store( diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 51f083b7eeb..49195e1a89a 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -94,7 +94,12 @@ class FlowManager(abc.ABC): def async_progress(self) -> List[Dict]: """Return the flows in progress.""" return [ - {"flow_id": flow.flow_id, "handler": flow.handler, "context": flow.context} + { + "flow_id": flow.flow_id, + "handler": flow.handler, + "context": flow.context, + "step_id": flow.cur_step["step_id"], + } for flow in self._progress.values() if flow.cur_step is not None ] @@ -253,6 +258,16 @@ class FlowHandler: # Set by developer VERSION = 1 + @property + def source(self) -> Optional[str]: + """Source that initialized the flow.""" + return self.context.get("source", None) + + @property + def show_advanced_options(self) -> bool: + """If we should show advanced options.""" + return self.context.get("show_advanced_options", False) + @callback def async_show_form( self, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e17aefac636..154fb024112 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -8,6 +8,7 @@ To update, run python3 -m script.hassfest FLOWS = [ "abode", "adguard", + "agent_dvr", "airly", "airvisual", "almond", @@ -16,14 +17,18 @@ FLOWS = [ "atag", "august", "axis", + "blebox", + "blink", "braviatv", "brother", + "bsblan", "cast", "cert_expiry", "coolmaster", "coronavirus", "daikin", "deconz", + "devolo_home_control", "dialogflow", "directv", "doorbird", @@ -33,8 +38,10 @@ FLOWS = [ "elkm1", "emulated_roku", "esphome", + "flick_electric", "flume", "flunearyou", + "forked_daapd", "freebox", "fritzbox", "garmin_connect", @@ -50,10 +57,13 @@ FLOWS = [ "harmony", "heos", "hisense_aehw4a1", + "home_connect", + "homekit", "homekit_controller", "homematicip_cloud", "huawei_lte", "hue", + "hunterdouglas_powerview", "iaqualink", "icloud", "ifttt", @@ -62,7 +72,9 @@ FLOWS = [ "ipp", "iqvia", "islamic_prayer_times", + "isy994", "izone", + "juicenet", "konnected", "life360", "lifx", @@ -71,11 +83,13 @@ FLOWS = [ "locative", "logi_circle", "luftdaten", + "lutron_caseta", "mailgun", "melcloud", "met", "meteo_france", "mikrotik", + "mill", "minecraft_server", "mobile_app", "monoprice", @@ -89,10 +103,13 @@ FLOWS = [ "nuheat", "nut", "nws", + "onvif", "opentherm_gw", "openuv", "owntracks", + "ozw", "panasonic_viera", + "pi_hole", "plaato", "plex", "point", @@ -115,6 +132,7 @@ FLOWS = [ "solarlog", "soma", "somfy", + "songpal", "sonos", "spotify", "starline", @@ -122,15 +140,18 @@ FLOWS = [ "tado", "tellduslive", "tesla", + "tibber", "toon", "totalconnect", "tplink", "traccar", "tradfri", "transmission", + "tuya", "twentemilieu", "twilio", "unifi", + "upb", "upnp", "velbus", "vera", @@ -138,9 +159,12 @@ FLOWS = [ "vilfo", "vizio", "wemo", + "wiffi", "withings", "wled", "wwlln", + "xiaomi_miio", + "zerproc", "zha", "zwave" ] diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 5dbef37d9bf..490ffdffeb1 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -53,6 +53,12 @@ SSDP = { "modelName": "Philips hue bridge 2015" } ], + "isy994": [ + { + "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1", + "manufacturer": "Universal Devices Inc." + } + ], "konnected": [ { "manufacturer": "konnected.io" @@ -70,6 +76,12 @@ SSDP = { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], + "songpal": [ + { + "manufacturer": "Sony Corporation", + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1" + } + ], "sonos": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" @@ -81,6 +93,14 @@ SSDP = { "manufacturer": "Synology" } ], + "upnp": [ + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + }, + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" + } + ], "wemo": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d6e4965c235..880bfedf400 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -10,8 +10,8 @@ ZEROCONF = { "axis", "doorbird" ], - "_coap._udp.local.": [ - "tradfri" + "_daap._tcp.local.": [ + "forked_daapd" ], "_elg._tcp.local.": [ "elgato" @@ -52,6 +52,7 @@ HOMEKIT = { "Healty Home Coach": "netatmo", "LIFX": "lifx", "Netatmo Relay": "netatmo", + "PowerView": "hunterdouglas_powerview", "Presence": "netatmo", "Rachio": "rachio", "TRADFRI": "tradfri", diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index eee891b7f88..fbe8ee62812 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,5 +1,6 @@ """Helper for aiohttp webclient stuff.""" import asyncio +import logging from ssl import SSLContext import sys from typing import Any, Awaitable, Optional, Union, cast @@ -12,10 +13,13 @@ import async_timeout from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, callback +from homeassistant.helpers.frame import MissingIntegrationFrame, get_integration_frame from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util +_LOGGER = logging.getLogger(__name__) + DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" DATA_CLIENTSESSION = "aiohttp_clientsession" @@ -64,12 +68,38 @@ def async_create_clientsession( connector = _async_get_connector(hass, verify_ssl) clientsession = aiohttp.ClientSession( - loop=hass.loop, - connector=connector, - headers={USER_AGENT: SERVER_SOFTWARE}, - **kwargs, + connector=connector, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) + async def patched_close() -> None: + """Mock close to avoid integrations closing our session.""" + try: + found_frame, integration, path = get_integration_frame() + except MissingIntegrationFrame: + # Did not source from an integration? Hard error. + raise RuntimeError( + "Detected closing of the Home Assistant aiohttp session in the Home Assistant core. " + "Please report this issue." + ) + + index = found_frame.filename.index(path) + if path == "custom_components/": + extra = " to the custom component author" + else: + extra = "" + + _LOGGER.warning( + "Detected integration that closes the Home Assistant aiohttp session. " + "Please report issue%s for %s using this method at %s, line %s: %s", + extra, + integration, + found_frame.filename[index:], + found_frame.lineno, + found_frame.line.strip(), + ) + + clientsession.close = patched_close # type: ignore + if auto_cleanup: _async_register_clientsession_shutdown(hass, clientsession) @@ -174,9 +204,7 @@ def _async_get_connector( else: ssl_context = False - connector = aiohttp.TCPConnector( - loop=hass.loop, enable_cleanup_closed=True, ssl=ssl_context - ) + connector = aiohttp.TCPConnector(enable_cleanup_closed=True, ssl=ssl_context) hass.data[key] = connector async def _async_close_connector(event: Event) -> None: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index e720887eb70..06c86d3aa1c 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -1,5 +1,6 @@ """Helper to deal with YAML + storage.""" from abc import ABC, abstractmethod +import asyncio import logging from typing import Any, Awaitable, Callable, Dict, List, Optional, cast @@ -107,8 +108,9 @@ class ObservableCollection(ABC): async def notify_change(self, change_type: str, item_id: str, item: dict) -> None: """Notify listeners of a change.""" self.logger.debug("%s %s: %s", change_type, item_id, item) - for listener in self.listeners: - await listener(change_type, item_id, item) + await asyncio.gather( + *[listener(change_type, item_id, item) for listener in self.listeners] + ) class YamlCollection(ObservableCollection): @@ -118,6 +120,8 @@ class YamlCollection(ObservableCollection): """Load the YAML collection. Overrides existing data.""" old_ids = set(self.data) + tasks = [] + for item in data: item_id = item[CONF_ID] @@ -131,11 +135,15 @@ class YamlCollection(ObservableCollection): event = CHANGE_ADDED self.data[item_id] = item - await self.notify_change(event, item_id, item) + tasks.append(self.notify_change(event, item_id, item)) for item_id in old_ids: + tasks.append( + self.notify_change(CHANGE_REMOVED, item_id, self.data.pop(item_id)) + ) - await self.notify_change(CHANGE_REMOVED, item_id, self.data.pop(item_id)) + if tasks: + await asyncio.gather(*tasks) class StorageCollection(ObservableCollection): @@ -169,7 +177,13 @@ class StorageCollection(ObservableCollection): for item in raw_storage["items"]: self.data[item[CONF_ID]] = item - await self.notify_change(CHANGE_ADDED, item[CONF_ID], item) + + await asyncio.gather( + *[ + self.notify_change(CHANGE_ADDED, item[CONF_ID], item) + for item in raw_storage["items"] + ] + ) @abstractmethod async def _process_create_data(self, data: dict) -> dict: @@ -240,8 +254,12 @@ class IDLessCollection(ObservableCollection): async def async_load(self, data: List[dict]) -> None: """Load the collection. Overrides existing data.""" - for item_id, item in list(self.data.items()): - await self.notify_change(CHANGE_REMOVED, item_id, item) + await asyncio.gather( + *[ + self.notify_change(CHANGE_REMOVED, item_id, item) + for item_id, item in list(self.data.items()) + ] + ) self.data.clear() @@ -250,7 +268,13 @@ class IDLessCollection(ObservableCollection): item_id = f"fakeid-{self.counter}" self.data[item_id] = item - await self.notify_change(CHANGE_ADDED, item_id, item) + + await asyncio.gather( + *[ + self.notify_change(CHANGE_ADDED, item_id, item) + for item_id, item in self.data.items() + ] + ) @callback diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 363d33b14ea..535de0304a0 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -137,6 +137,32 @@ async def async_or_from_config( return if_or_condition +async def async_not_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Create multi condition matcher using 'NOT'.""" + if config_validation: + config = cv.NOT_CONDITION_SCHEMA(config) + checks = [ + await async_from_config(hass, entry, False) for entry in config["conditions"] + ] + + def if_not_condition( + hass: HomeAssistant, variables: TemplateVarsType = None + ) -> bool: + """Test not condition.""" + try: + for check in checks: + if check(hass, variables): + return False + except Exception as ex: # pylint: disable=broad-except + _LOGGER.warning("Error during not-condition: %s", ex) + + return True + + return if_not_condition + + def numeric_state( hass: HomeAssistant, entity: Union[None, str, State], @@ -510,7 +536,7 @@ async def async_validate_condition_config( ) -> ConfigType: """Validate config.""" condition = config[CONF_CONDITION] - if condition in ("and", "or"): + if condition in ("and", "not", "or"): conditions = [] for sub_cond in config["conditions"]: sub_cond = await async_validate_condition_config(hass, sub_cond) @@ -537,7 +563,7 @@ def async_extract_entities(config: ConfigType) -> Set[str]: config = to_process.popleft() condition = config[CONF_CONDITION] - if condition in ("and", "or"): + if condition in ("and", "not", "or"): to_process.extend(config["conditions"]) continue @@ -559,7 +585,7 @@ def async_extract_devices(config: ConfigType) -> Set[str]: config = to_process.popleft() condition = config[CONF_CONDITION] - if condition in ("and", "or"): + if condition in ("and", "not", "or"): to_process.extend(config["conditions"]) continue diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 323c6907411..81881d943cd 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -33,6 +33,8 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + await self.async_set_unique_id(self._domain, raise_on_progress=False) + return await self.async_step_confirm() async def async_step_confirm(self, user_input=None): @@ -40,10 +42,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): if user_input is None: return self.async_show_form(step_id="confirm") - if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context - and self.context.get("source") != config_entries.SOURCE_DISCOVERY - ): + if self.source == config_entries.SOURCE_USER: # Get current discovered entries. in_progress = self._async_in_progress() @@ -67,6 +66,8 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + await self.async_set_unique_id(self._domain) + return await self.async_step_confirm() async_step_zeroconf = async_step_discovery diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 0c5a5c3873e..712ea9f105c 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -21,6 +21,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.network import get_url from .aiohttp_client import async_get_clientsession @@ -117,7 +118,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): @property def redirect_uri(self) -> str: """Return the redirect uri.""" - return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" # type: ignore + return f"{get_url(self.hass)}{AUTH_CALLBACK_PATH}" async def async_generate_authorize_url(self, flow_id: str) -> str: """Generate a url for the user to authorize.""" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7bb3223f3d7..32121958b03 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -465,6 +465,15 @@ def string(value: Any) -> str: return str(value) +def string_with_no_html(value: Any) -> str: + """Validate that the value is a string without HTML.""" + value = string(value) + regex = re.compile(r"<[a-z][\s\S]*>") + if regex.search(value): + raise vol.Invalid("the string should not contain HTML") + return str(value) + + def temperature_unit(value: Any) -> str: """Validate and transform temperature unit.""" value = str(value).upper() @@ -924,6 +933,17 @@ OR_CONDITION_SCHEMA = vol.Schema( } ) +NOT_CONDITION_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "not", + vol.Required("conditions"): vol.All( + ensure_list, + # pylint: disable=unnecessary-lambda + [lambda value: CONDITION_SCHEMA(value)], + ), + } +) + DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( { vol.Required(CONF_CONDITION): "device", @@ -945,6 +965,7 @@ CONDITION_SCHEMA: vol.Schema = key_value_schemas( "zone": ZONE_CONDITION_SCHEMA, "and": AND_CONDITION_SCHEMA, "or": OR_CONDITION_SCHEMA, + "not": NOT_CONDITION_SCHEMA, "device": DEVICE_CONDITION_SCHEMA, }, ) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 951b6d4c748..fe6420750c6 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -49,7 +49,13 @@ class FlowManagerIndexView(_BaseFlowManagerView): """View to create config flows.""" @RequestDataValidator( - vol.Schema({vol.Required("handler"): vol.Any(str, list)}, extra=vol.ALLOW_EXTRA) + vol.Schema( + { + vol.Required("handler"): vol.Any(str, list), + vol.Optional("show_advanced_options", default=False): cv.boolean, + }, + extra=vol.ALLOW_EXTRA, + ) ) async def post(self, request, data): """Handle a POST request.""" @@ -60,7 +66,11 @@ class FlowManagerIndexView(_BaseFlowManagerView): try: result = await self._flow_mgr.async_init( - handler, context={"source": config_entries.SOURCE_USER} + handler, + context={ + "source": config_entries.SOURCE_USER, + "show_advanced_options": data["show_advanced_options"], + }, ) except data_entry_flow.UnknownHandler: return self.json_message("Invalid handler specified", HTTP_NOT_FOUND) diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 6206081dc8c..3f297dcbbe8 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -21,7 +21,7 @@ class Debouncer: """Initialize debounce. immediate: indicate if the function needs to be called right away and - wait 0.3s until executing next invocation. + wait until executing next invocation. function: optional and can be instantiated later. """ self.hass = hass diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ef85ac953f6..8fbb81962ff 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,17 +1,21 @@ """Provide a way to connect entities belonging to one device.""" -from asyncio import Event from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import uuid import attr -from homeassistant.core import callback -from homeassistant.loader import bind_hass +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import Event, callback +from .debounce import Debouncer +from .singleton import singleton from .typing import HomeAssistantType +if TYPE_CHECKING: + from . import entity_registry + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -22,6 +26,7 @@ EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 +CLEANUP_DELAY = 10 CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" @@ -32,19 +37,24 @@ CONNECTION_ZIGBEE = "zigbee" class DeviceEntry: """Device Registry Entry.""" - config_entries = attr.ib(type=set, converter=set, default=attr.Factory(set)) - connections = attr.ib(type=set, converter=set, default=attr.Factory(set)) - identifiers = attr.ib(type=set, converter=set, default=attr.Factory(set)) - manufacturer = attr.ib(type=str, default=None) - model = attr.ib(type=str, default=None) - name = attr.ib(type=str, default=None) - sw_version = attr.ib(type=str, default=None) - via_device_id = attr.ib(type=str, default=None) - area_id = attr.ib(type=str, default=None) - name_by_user = attr.ib(type=str, default=None) - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + config_entries: Set[str] = attr.ib(converter=set, default=attr.Factory(set)) + connections: Set[Tuple[str, str]] = attr.ib( + converter=set, default=attr.Factory(set) + ) + identifiers: Set[Tuple[str, str]] = attr.ib( + converter=set, default=attr.Factory(set) + ) + manufacturer: str = attr.ib(default=None) + model: str = attr.ib(default=None) + name: str = attr.ib(default=None) + sw_version: str = attr.ib(default=None) + via_device_id: str = attr.ib(default=None) + area_id: str = attr.ib(default=None) + name_by_user: str = attr.ib(default=None) + entry_type: str = attr.ib(default=None) + id: str = attr.ib(default=attr.Factory(lambda: uuid.uuid4().hex)) # This value is not stored, just used to keep track of events to fire. - is_new = attr.ib(type=bool, default=False) + is_new: bool = attr.ib(default=False) def format_mac(mac: str) -> str: @@ -105,6 +115,7 @@ class DeviceRegistry: model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, + entry_type=_UNDEF, via_device=None, ): """Get device. Create if it doesn't exist.""" @@ -144,6 +155,7 @@ class DeviceRegistry: model=model, name=name, sw_version=sw_version, + entry_type=entry_type, ) @callback @@ -189,6 +201,7 @@ class DeviceRegistry: model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, + entry_type=_UNDEF, via_device_id=_UNDEF, area_id=_UNDEF, name_by_user=_UNDEF, @@ -236,6 +249,7 @@ class DeviceRegistry: ("model", model), ("name", name), ("sw_version", sw_version), + ("entry_type", entry_type), ("via_device_id", via_device_id), ): if value is not _UNDEF and value != getattr(old, attr_name): @@ -277,6 +291,8 @@ class DeviceRegistry: async def async_load(self): """Load the device registry.""" + async_setup_cleanup(self.hass, self) + data = await self._store.async_load() devices = OrderedDict() @@ -291,6 +307,8 @@ class DeviceRegistry: model=device["model"], name=device["name"], sw_version=device["sw_version"], + # Introduced in 0.110 + entry_type=device.get("entry_type"), id=device["id"], # Introduced in 0.79 # renamed in 0.95 @@ -323,6 +341,7 @@ class DeviceRegistry: "model": entry.model, "name": entry.name, "sw_version": entry.sw_version, + "entry_type": entry.entry_type, "id": entry.id, "via_device_id": entry.via_device_id, "area_id": entry.area_id, @@ -336,16 +355,8 @@ class DeviceRegistry: @callback def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" - remove = [] - for dev_id, device in self.devices.items(): - if device.config_entries == {config_entry_id}: - remove.append(dev_id) - else: - self._async_update_device( - dev_id, remove_config_entry_id=config_entry_id - ) - for dev_id in remove: - self.async_remove_device(dev_id) + for device in list(self.devices.values()): + self._async_update_device(device.id, remove_config_entry_id=config_entry_id) @callback def async_clear_area_id(self, area_id: str) -> None: @@ -355,27 +366,12 @@ class DeviceRegistry: self._async_update_device(dev_id, area_id=None) -@bind_hass +@singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: - """Return device registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) - - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = Event() - - reg = DeviceRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, Event): - evt = reg_or_evt - await evt.wait() - return cast(DeviceRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(DeviceRegistry, reg_or_evt) + """Create entity registry.""" + reg = DeviceRegistry(hass) + await reg.async_load() + return reg @callback @@ -394,3 +390,69 @@ def async_entries_for_config_entry( for device in registry.devices.values() if config_entry_id in device.config_entries ] + + +@callback +def async_cleanup( + hass: HomeAssistantType, + dev_reg: DeviceRegistry, + ent_reg: "entity_registry.EntityRegistry", +) -> None: + """Clean up device registry.""" + # Find all devices that are no longer referenced in the entity registry. + referenced = {entry.device_id for entry in ent_reg.entities.values()} + orphan = set(dev_reg.devices) - referenced + + for dev_id in orphan: + dev_reg.async_remove_device(dev_id) + + # Find all referenced config entries that no longer exist + # This shouldn't happen but have not been able to track down the bug :( + config_entry_ids = {entry.entry_id for entry in hass.config_entries.async_entries()} + + for device in list(dev_reg.devices.values()): + for config_entry_id in device.config_entries: + if config_entry_id not in config_entry_ids: + dev_reg.async_update_device( + device.id, remove_config_entry_id=config_entry_id + ) + + +@callback +def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> None: + """Clean up device registry when entities removed.""" + from . import entity_registry # pylint: disable=import-outside-toplevel + + async def cleanup(): + """Cleanup.""" + ent_reg = await entity_registry.async_get_registry(hass) + async_cleanup(hass, dev_reg, ent_reg) + + debounced_cleanup = Debouncer( + hass, _LOGGER, cooldown=CLEANUP_DELAY, immediate=False, function=cleanup + ) + + async def entity_registry_changed(event: Event) -> None: + """Handle entity updated or removed.""" + if ( + event.data["action"] == "update" + and "device_id" not in event.data["changes"] + ) or event.data["action"] == "create": + return + + await debounced_cleanup.async_call() + + if hass.is_running: + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed + ) + return + + async def startup_clean(event: Event) -> None: + """Clean up on startup.""" + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed + ) + await debounced_cleanup.async_call() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 738b49f4c54..b5d36f6a2f5 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -493,13 +493,14 @@ class Entity(ABC): async def async_remove(self) -> None: """Remove entity from Home Assistant.""" assert self.hass is not None - await self.async_internal_will_remove_from_hass() - await self.async_will_remove_from_hass() if self._on_remove is not None: while self._on_remove: self._on_remove.pop()() + await self.async_internal_will_remove_from_hass() + await self.async_will_remove_from_hass() + self.hass.states.async_remove(self.entity_id, context=self._context) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4c30457b62c..fb7762fb9ae 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -201,7 +201,7 @@ class EntityComponent: name: str, schema: Union[Dict[str, Any], vol.Schema], func: str, - required_features: Optional[int] = None, + required_features: Optional[List[int]] = None, ) -> None: """Register an entity service.""" if isinstance(schema, dict): @@ -270,9 +270,15 @@ class EntityComponent: async def async_remove_entity(self, entity_id: str) -> None: """Remove an entity managed by one of the platforms.""" + found = None + for platform in self._platforms.values(): if entity_id in platform.entities: - await platform.async_remove_entity(entity_id) + found = platform + break + + if found: + await found.async_remove_entity(entity_id) async def async_prepare_reload(self, *, skip_reset: bool = False) -> Optional[dict]: """Prepare reloading this entity component. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e4d52aaa3a1..30b07c98252 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -353,6 +353,7 @@ class EntityPlatform: "model", "name", "sw_version", + "entry_type", "via_device", ): if key in device_info: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 10de8564fca..de4f5eeb297 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,7 +7,6 @@ The Entity Registry will persist itself 10 seconds after a new entity is registered. Registering a new entity while a timer is in progress resets the timer. """ -import asyncio from collections import OrderedDict import logging from typing import ( @@ -35,10 +34,10 @@ from homeassistant.const import ( ) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml +from .singleton import singleton from .typing import HomeAssistantType if TYPE_CHECKING: @@ -429,6 +428,12 @@ class EntityRegistry: if data is not None: for entity in data["entities"]: + # Some old installations can have some bad entities. + # Filter them out as they cause errors down the line. + # Can be removed in Jan 2021 + if not valid_entity_id(entity["entity_id"]): + continue + entities[entity["entity_id"]] = RegistryEntry( entity_id=entity["entity_id"], config_entry_id=entity.get("config_entry_id"), @@ -491,27 +496,12 @@ class EntityRegistry: self.async_remove(entity_id) -@bind_hass +@singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: - """Return entity registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) - - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = asyncio.Event() - - reg = EntityRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, asyncio.Event): - evt = reg_or_evt - await evt.wait() - return cast(EntityRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(EntityRegistry, reg_or_evt) + """Create entity registry.""" + reg = EntityRegistry(hass) + await reg.async_load() + return reg @callback @@ -621,4 +611,4 @@ async def async_migrate_entries( updates = entry_callback(entry) if updates is not None: - ent_reg.async_update_entity(entry.entity_id, **updates) # type: ignore + ent_reg.async_update_entity(entry.entity_id, **updates) diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index a9c3ab27ead..f8dd83ccfcc 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -12,7 +12,8 @@ CONF_EXCLUDE_DOMAINS = "exclude_domains" CONF_EXCLUDE_ENTITIES = "exclude_entities" -def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: +def convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: + """Convert the filter schema into a filter.""" filt = generate_filter( config[CONF_INCLUDE_DOMAINS], config[CONF_INCLUDE_ENTITIES], @@ -24,22 +25,21 @@ def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: return filt -FILTER_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_INCLUDE_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids, - } - ), - _convert_filter, +BASE_FILTER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_INCLUDE_DOMAINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids, + } ) +FILTER_SCHEMA = vol.All(BASE_FILTER_SCHEMA, convert_filter) + def generate_filter( include_domains: List[str], diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py new file mode 100644 index 00000000000..057152e4def --- /dev/null +++ b/homeassistant/helpers/frame.py @@ -0,0 +1,36 @@ +"""Provide frame helper for finding the current frame context.""" +from traceback import FrameSummary, extract_stack +from typing import Tuple + +from homeassistant.exceptions import HomeAssistantError + + +def get_integration_frame() -> Tuple[FrameSummary, str, str]: + """Return the frame, integration and integration path of the current stack frame.""" + found_frame = None + + for frame in reversed(extract_stack()): + for path in ("custom_components/", "homeassistant/components/"): + try: + index = frame.filename.index(path) + found_frame = frame + break + except ValueError: + continue + + if found_frame is not None: + break + + if found_frame is None: + raise MissingIntegrationFrame + + start = index + len(path) + end = found_frame.filename.index("/", start) + + integration = found_frame.filename[start:end] + + return found_frame, integration, path + + +class MissingIntegrationFrame(HomeAssistantError): + """Raised when no integration is found in the frame.""" diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py new file mode 100644 index 00000000000..1df039da47a --- /dev/null +++ b/homeassistant/helpers/instance_id.py @@ -0,0 +1,31 @@ +"""Helper to create a unique instance ID.""" +from typing import Dict, Optional +import uuid + +from homeassistant.core import HomeAssistant + +from . import singleton, storage + +DATA_KEY = "core.uuid" +DATA_VERSION = 1 + +LEGACY_UUID_FILE = ".uuid" + + +@singleton.singleton(DATA_KEY) +async def async_get(hass: HomeAssistant) -> str: + """Get unique ID for the hass instance.""" + store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) + + data: Optional[Dict[str, str]] = await storage.async_migrator( # type: ignore + hass, hass.config.path(LEGACY_UUID_FILE), store, + ) + + if data is not None: + return data["uuid"] + + data = {"uuid": uuid.uuid4().hex} + + await store.async_save(data) + + return data["uuid"] diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index a446b575077..cebe0318496 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,38 +1,225 @@ """Network helpers.""" from ipaddress import ip_address -from typing import Optional, cast +from typing import cast import yarl -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from homeassistant.util.network import is_local +from homeassistant.util.network import ( + is_ip_address, + is_local, + is_loopback, + is_private, + normalize_url, +) + +TYPE_URL_INTERNAL = "internal_url" +TYPE_URL_EXTERNAL = "external_url" + + +class NoURLAvailableError(HomeAssistantError): + """An URL to the Home Assistant instance is not available.""" @bind_hass -@callback -def async_get_external_url(hass: HomeAssistant) -> Optional[str]: - """Get external url of this instance. +def get_url( + hass: HomeAssistant, + *, + require_ssl: bool = False, + require_standard_port: bool = False, + allow_internal: bool = True, + allow_external: bool = True, + allow_cloud: bool = True, + allow_ip: bool = True, + prefer_external: bool = False, + prefer_cloud: bool = False, +) -> str: + """Get a URL to this instance.""" + order = [TYPE_URL_INTERNAL, TYPE_URL_EXTERNAL] + if prefer_external: + order.reverse() - Note: currently it takes 30 seconds after Home Assistant starts for - cloud.async_remote_ui_url to work. - """ + # Try finding an URL in the order specified + for url_type in order: + + if allow_internal and url_type == TYPE_URL_INTERNAL: + try: + return _get_internal_url( + hass, + allow_ip=allow_ip, + require_ssl=require_ssl, + require_standard_port=require_standard_port, + ) + except NoURLAvailableError: + pass + + if allow_external and url_type == TYPE_URL_EXTERNAL: + try: + return _get_external_url( + hass, + allow_cloud=allow_cloud, + allow_ip=allow_ip, + prefer_cloud=prefer_cloud, + require_ssl=require_ssl, + require_standard_port=require_standard_port, + ) + except NoURLAvailableError: + pass + + # We have to be honest now, we have no viable option available + raise NoURLAvailableError + + +@bind_hass +def _get_internal_url( + hass: HomeAssistant, + *, + allow_ip: bool = True, + require_ssl: bool = False, + require_standard_port: bool = False, +) -> str: + """Get internal URL of this instance.""" + if hass.config.internal_url: + internal_url = yarl.URL(hass.config.internal_url) + if ( + (not require_ssl or internal_url.scheme == "https") + and (not require_standard_port or internal_url.is_default_port()) + and (allow_ip or not is_ip_address(str(internal_url.host))) + ): + return normalize_url(str(internal_url)) + + # Fallback to old base_url + try: + return _get_deprecated_base_url( + hass, + internal=True, + allow_ip=allow_ip, + require_ssl=require_ssl, + require_standard_port=require_standard_port, + ) + except NoURLAvailableError: + pass + + # Fallback to detected local IP + if allow_ip and not ( + require_ssl or hass.config.api is None or hass.config.api.use_ssl + ): + ip_url = yarl.URL.build( + scheme="http", host=hass.config.api.local_ip, port=hass.config.api.port + ) + if not is_loopback(ip_address(ip_url.host)) and ( + not require_standard_port or ip_url.is_default_port() + ): + return normalize_url(str(ip_url)) + + raise NoURLAvailableError + + +@bind_hass +def _get_external_url( + hass: HomeAssistant, + *, + allow_cloud: bool = True, + allow_ip: bool = True, + prefer_cloud: bool = False, + require_ssl: bool = False, + require_standard_port: bool = False, +) -> str: + """Get external URL of this instance.""" + if prefer_cloud and allow_cloud: + try: + return _get_cloud_url(hass) + except NoURLAvailableError: + pass + + if hass.config.external_url: + external_url = yarl.URL(hass.config.external_url) + if ( + (allow_ip or not is_ip_address(str(external_url.host))) + and (not require_standard_port or external_url.is_default_port()) + and ( + not require_ssl + or ( + external_url.scheme == "https" + and not is_ip_address(str(external_url.host)) + ) + ) + ): + return normalize_url(str(external_url)) + + try: + return _get_deprecated_base_url( + hass, + allow_ip=allow_ip, + require_ssl=require_ssl, + require_standard_port=require_standard_port, + ) + except NoURLAvailableError: + pass + + if allow_cloud: + try: + return _get_cloud_url(hass) + except NoURLAvailableError: + pass + + raise NoURLAvailableError + + +@bind_hass +def _get_cloud_url(hass: HomeAssistant) -> str: + """Get external Home Assistant Cloud URL of this instance.""" if "cloud" in hass.config.components: try: return cast(str, hass.components.cloud.async_remote_ui_url()) except hass.components.cloud.CloudNotAvailable: pass - if hass.config.api is None: - return None + raise NoURLAvailableError - base_url = yarl.URL(hass.config.api.base_url) - try: - if is_local(ip_address(base_url.host)): - return None - except ValueError: - # ip_address raises ValueError if host is not an IP address - pass +@bind_hass +def _get_deprecated_base_url( + hass: HomeAssistant, + *, + internal: bool = False, + allow_ip: bool = True, + require_ssl: bool = False, + require_standard_port: bool = False, +) -> str: + """Work with the deprecated `base_url`, used as fallback.""" + if hass.config.api is None or not hass.config.api.deprecated_base_url: + raise NoURLAvailableError - return str(base_url) + base_url = yarl.URL(hass.config.api.deprecated_base_url) + # Rules that apply to both internal and external + if ( + (allow_ip or not is_ip_address(str(base_url.host))) + and (not require_ssl or base_url.scheme == "https") + and (not require_standard_port or base_url.is_default_port()) + ): + # Check to ensure an internal URL + if internal and ( + str(base_url.host).endswith(".local") + or ( + is_ip_address(str(base_url.host)) + and not is_loopback(ip_address(base_url.host)) + and is_private(ip_address(base_url.host)) + ) + ): + return normalize_url(str(base_url)) + + # Check to ensure an external URL (a little) + if ( + not internal + and not str(base_url.host).endswith(".local") + and not ( + is_ip_address(str(base_url.host)) + and is_local(ip_address(str(base_url.host))) + ) + ): + return normalize_url(str(base_url)) + + raise NoURLAvailableError diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index a75f862467e..ce52d188540 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -545,19 +545,25 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: reg = await hass.helpers.entity_registry.async_get_registry() + authorized = False + for entity in reg.entities.values(): if entity.platform != domain: continue if user.permissions.check_entity(entity.entity_id, POLICY_CONTROL): - return await service_handler(call) + authorized = True + break - raise Unauthorized( - context=call.context, - permission=POLICY_CONTROL, - user_id=call.context.user_id, - perm_category=CAT_ENTITIES, - ) + if not authorized: + raise Unauthorized( + context=call.context, + permission=POLICY_CONTROL, + user_id=call.context.user_id, + perm_category=CAT_ENTITIES, + ) + + return await service_handler(call) return check_permissions diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py new file mode 100644 index 00000000000..82b666be40a --- /dev/null +++ b/homeassistant/helpers/singleton.py @@ -0,0 +1,46 @@ +"""Helper to help coordinating calls.""" +import asyncio +import functools +from typing import Awaitable, Callable, TypeVar, cast + +from homeassistant.core import HomeAssistant +from homeassistant.loader import bind_hass + +T = TypeVar("T") + +FUNC = Callable[[HomeAssistant], Awaitable[T]] + + +def singleton(data_key: str) -> Callable[[FUNC], FUNC]: + """Decorate a function that should be called once per instance. + + Result will be cached and simultaneous calls will be handled. + """ + + def wrapper(func: FUNC) -> FUNC: + """Wrap a function with caching logic.""" + + @bind_hass + @functools.wraps(func) + async def wrapped(hass: HomeAssistant) -> T: + obj_or_evt = hass.data.get(data_key) + + if not obj_or_evt: + evt = hass.data[data_key] = asyncio.Event() + + result = await func(hass) + + hass.data[data_key] = result + evt.set() + return cast(T, result) + + if isinstance(obj_or_evt, asyncio.Event): + evt = obj_or_evt + await evt.wait() + return cast(T, hass.data.get(data_key)) + + return cast(T, obj_or_evt) + + return wrapped + + return wrapper diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 00df728fb36..b5ac942bf2f 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -20,29 +20,32 @@ _LOGGER = logging.getLogger(__name__) @bind_hass async def async_migrator( - hass, - old_path, - store, - *, - old_conf_load_func=json_util.load_json, - old_conf_migrate_func=None, + hass, old_path, store, *, old_conf_load_func=None, old_conf_migrate_func=None, ): """Migrate old data to a store and then load data. async def old_conf_migrate_func(old_data) """ + store_data = await store.async_load() + + # If we already have store data we have already migrated in the past. + if store_data is not None: + return store_data def load_old_config(): """Load old config.""" if not os.path.isfile(old_path): return None - return old_conf_load_func(old_path) + if old_conf_load_func is not None: + return old_conf_load_func(old_path) + + return json_util.load_json(old_path) config = await hass.async_add_executor_job(load_old_config) if config is None: - return await store.async_load() + return None if old_conf_migrate_func is not None: config = await old_conf_migrate_func(config) diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 26ef3929a3f..293a6e7bcef 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -14,6 +14,7 @@ from .typing import HomeAssistantType async def async_get_system_info(hass: HomeAssistantType) -> Dict: """Return info about the system.""" info_object = { + "installation_type": "Unknown", "version": current_version, "dev": "dev" in current_version, "hassio": hass.components.hassio.is_hassio(), @@ -33,4 +34,26 @@ async def async_get_system_info(hass: HomeAssistantType) -> Dict: elif platform.system() == "Linux": info_object["docker"] = os.path.isfile("/.dockerenv") + # Determine installation type on current data + if info_object["docker"]: + info_object["installation_type"] = "Home Assistant Core on Docker" + elif is_virtual_env(): + info_object[ + "installation_type" + ] = "Home Assistant Core in a Python Virtual Environment" + + # Enrich with Supervisor information + if hass.components.hassio.is_hassio(): + info = hass.components.hassio.get_info() + host = hass.components.hassio.get_host_info() + + info_object["supervisor"] = info.get("supervisor") + info_object["host_os"] = host.get("operating_system") + info_object["chassis"] = host.get("chassis") + + if info.get("hassos") is not None: + info_object["installation_type"] = "Home Assistant" + else: + info_object["installation_type"] = "Home Assistant Supervised" + return info_object diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3f9924d00d5..b5e62ee2fcd 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,5 +1,6 @@ """Template helper methods for rendering strings with Home Assistant data.""" import base64 +import collections.abc from datetime import datetime from functools import wraps import json @@ -43,8 +44,8 @@ _ENVIRONMENT = "template.environment" _RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M) _RE_GET_ENTITIES = re.compile( - r"(?:(?:states\.|(?:is_state|is_state_attr|state_attr|states)" - r"\((?:[\ \'\"]?))([\w]+\.[\w]+)|([\w]+))", + r"(?:(?:states\.|(?Pis_state|is_state_attr|state_attr|states|expand)" + r"\((?:[\ \'\"]?))(?P[\w]+\.[\w]+)|(?P[\w]+))", re.I | re.M, ) _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{") @@ -75,7 +76,9 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: def extract_entities( - template: Optional[str], variables: Optional[Dict[str, Any]] = None + hass: HomeAssistantType, + template: Optional[str], + variables: Optional[Dict[str, Any]] = None, ) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" if template is None or _RE_JINJA_DELIMITERS.search(template) is None: @@ -84,27 +87,30 @@ def extract_entities( if _RE_NONE_ENTITIES.search(template): return MATCH_ALL - extraction = _RE_GET_ENTITIES.findall(template) extraction_final = [] - for result in extraction: + for result in _RE_GET_ENTITIES.finditer(template): if ( - result[0] == "trigger.entity_id" + result.group("entity_id") == "trigger.entity_id" and variables and "trigger" in variables and "entity_id" in variables["trigger"] ): extraction_final.append(variables["trigger"]["entity_id"]) - elif result[0]: - extraction_final.append(result[0]) + elif result.group("entity_id"): + if result.group("func") == "expand": + for entity in expand(hass, result.group("entity_id")): + extraction_final.append(entity.entity_id) + + extraction_final.append(result.group("entity_id")) if ( variables - and result[1] in variables - and isinstance(variables[result[1]], str) - and valid_entity_id(variables[result[1]]) + and result.group("variable") in variables + and isinstance(variables[result.group("variable")], str) + and valid_entity_id(variables[result.group("variable")]) ): - extraction_final.append(variables[result[1]]) + extraction_final.append(variables[result.group("variable")]) if extraction_final: return list(set(extraction_final)) @@ -196,7 +202,7 @@ class Template: self, variables: Optional[Dict[str, Any]] = None ) -> Union[str, List[str]]: """Extract all entities for state_changed listener.""" - return extract_entities(self.template, variables) + return extract_entities(self.hass, self.template, variables) def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template.""" @@ -503,7 +509,7 @@ def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]: continue elif isinstance(entity, State): entity_id = entity.entity_id - elif isinstance(entity, Iterable): + elif isinstance(entity, collections.abc.Iterable): search += entity continue else: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 3c7e4699127..ed5545b3c28 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -26,7 +26,6 @@ from typing import ( ) # Typing imports that create a circular dependency -# pylint: disable=unused-import if TYPE_CHECKING: from homeassistant.core import HomeAssistant @@ -243,21 +242,16 @@ class Integration: """Return documentation.""" return cast(str, self.manifest.get("documentation")) + @property + def issue_tracker(self) -> Optional[str]: + """Return issue tracker link.""" + return cast(str, self.manifest.get("issue_tracker")) + @property def quality_scale(self) -> Optional[str]: """Return Integration Quality Scale.""" return cast(str, self.manifest.get("quality_scale")) - @property - def logo(self) -> Optional[str]: - """Return Integration Logo.""" - return cast(str, self.manifest.get("logo")) - - @property - def icon(self) -> Optional[str]: - """Return Integration Icon.""" - return cast(str, self.manifest.get("icon")) - @property def is_built_in(self) -> bool: """Test if package is a built-in integration.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b7e70914714..c59502846b6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,24 +8,24 @@ attrs==19.3.0 bcrypt==3.1.7 certifi>=2020.4.5.1 ciso8601==2.1.3 -cryptography==2.9 +cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 hass-nabucasa==0.34.2 -home-assistant-frontend==20200427.2 +home-assistant-frontend==20200519.0 importlib-metadata==1.6.0 jinja2>=2.11.1 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 -pytz>=2019.03 +pytz>=2020.1 pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.16 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.25.1 +zeroconf==0.26.1 pycryptodome>=3.6.6 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 627f5b9d976..8b4c6a446f2 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,10 +1,11 @@ """Script to check the configuration file.""" import argparse from collections import OrderedDict +from collections.abc import Mapping, Sequence from glob import glob import logging import os -from typing import Any, Callable, Dict, List, Sequence, Tuple +from typing import Any, Callable, Dict, List, Tuple from unittest.mock import patch from homeassistant import bootstrap, core @@ -252,7 +253,7 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): indent_str = indent_count * " " if listi or isinstance(layer, list): indent_str = indent_str[:-1] + "-" - if isinstance(layer, Dict): + if isinstance(layer, Mapping): for key, value in sorted(layer.items(), key=sort_dict_key): if isinstance(value, (dict, list)): print(indent_str, str(key) + ":", line_info(value, **kwargs)) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 82b7e1be039..70321d364b8 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -201,8 +201,12 @@ async def _async_setup_component( await asyncio.sleep(0) await hass.config_entries.flow.async_wait_init_flow_finish(domain) - for entry in hass.config_entries.async_entries(domain): - await entry.async_setup(hass, integration=integration) + await asyncio.gather( + *[ + entry.async_setup(hass, integration=integration) + for entry in hass.config_entries.async_entries(domain) + ] + ) hass.config.components.add(domain) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 129b633fbf3..9af19d7f9e1 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -15,6 +15,45 @@ "paused": "Paused", "home": "Home", "not_home": "Away" + }, + "config_flow": { + "title": { + "oauth2_pick_implementation": "Pick Authentication Method", + "via_hassio_addon": "{name} via Home Assistant add-on" + }, + "description": { + "confirm_setup": "Do you want to start set up?" + }, + "data": { + "email": "Email", + "username": "Username", + "password": "Password", + "host": "Host", + "ip": "IP address", + "port": "Port", + "usb_path": "USB Device Path", + "access_token": "Access Token", + "api_key": "API Key" + }, + "create_entry": { + "authenticated": "Successfully authenticated" + }, + "error": { + "invalid_api_key": "Invalid API key", + "invalid_access_token": "Invalid access token", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "already_configured_account": "Account is already configured", + "already_configured_service": "Service is already configured", + "already_configured_device": "Device is already configured", + "no_devices_found": "No devices found on the network", + "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth2_authorize_url_timeout": "Timeout generating authorize url." + } } } } diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index ee423a33b52..135de94a513 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -24,11 +24,9 @@ import slugify as unicode_slug from .dt import as_local, utcnow -# pylint: disable=invalid-name T = TypeVar("T") -U = TypeVar("U") -ENUM_T = TypeVar("ENUM_T", bound=enum.Enum) -# pylint: enable=invalid-name +U = TypeVar("U") # pylint: disable=invalid-name +ENUM_T = TypeVar("ENUM_T", bound=enum.Enum) # pylint: disable=invalid-name RE_SANITIZE_FILENAME = re.compile(r"(~|\.\.|/|\\)") RE_SANITIZE_PATH = re.compile(r"(~|\.(\.)+)") @@ -214,7 +212,6 @@ class Throttle: If we cannot acquire the lock, it is running so return None. """ - # pylint: disable=protected-access if hasattr(method, "__self__"): host = getattr(method, "__self__") elif is_func: @@ -222,12 +219,14 @@ class Throttle: else: host = args[0] if args else wrapper + # pylint: disable=protected-access # to _throttle if not hasattr(host, "_throttle"): host._throttle = {} if id(self) not in host._throttle: host._throttle[id(self)] = [threading.Lock(), None] throttle = host._throttle[id(self)] + # pylint: enable=protected-access if not throttle[0].acquire(False): return throttled_value() diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 8efad848e87..9b31aa18ef7 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -244,9 +244,11 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> Lis return res -# pylint: disable=redefined-outer-name def find_next_time_expression_time( - now: dt.datetime, seconds: List[int], minutes: List[int], hours: List[int] + now: dt.datetime, # pylint: disable=redefined-outer-name + seconds: List[int], + minutes: List[int], + hours: List[int], ) -> dt.datetime: """Find the next datetime from now for which the time expression matches. diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index c5da910fae1..51d7c26a554 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -56,7 +56,6 @@ def save_json( try: json_data = json.dumps(data, sort_keys=True, indent=4, cls=encoder) except TypeError: - # pylint: disable=no-member msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" _LOGGER.error(msg) raise SerializationError(msg) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 7bda3728612..85db07e2d42 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -80,7 +80,6 @@ def distance( # Author: https://github.com/maurycyp # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE -# pylint: disable=invalid-name def vincenty( point1: Tuple[float, float], point2: Tuple[float, float], miles: bool = False ) -> Optional[float]: @@ -96,6 +95,7 @@ def vincenty( if point1[0] == point2[0] and point1[1] == point2[1]: return 0.0 + # pylint: disable=invalid-name U1 = math.atan((1 - FLATTENING) * math.tan(math.radians(point1[0]))) U2 = math.atan((1 - FLATTENING) * math.tan(math.radians(point2[0]))) L = math.radians(point2[1] - point1[1]) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index abadb613168..ae59e9bf4f9 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -24,7 +24,6 @@ class HideSensitiveDataFilter(logging.Filter): return True -# pylint: disable=invalid-name class AsyncHandler: """Logging handler wrapper to add an async layer.""" @@ -36,6 +35,7 @@ class AsyncHandler: self._thread = threading.Thread(target=self._process) # Delegate from handler + # pylint: disable=invalid-name self.setLevel = handler.setLevel self.setFormatter = handler.setFormatter self.addFilter = handler.addFilter @@ -94,7 +94,7 @@ class AsyncHandler: except asyncio.CancelledError: self.handler.close() - def createLock(self) -> None: + def createLock(self) -> None: # pylint: disable=invalid-name """Ignore lock stuff.""" def acquire(self) -> None: @@ -124,27 +124,29 @@ class AsyncHandler: self.handler.set_name(name) # type: ignore +def log_exception(format_err: Callable[..., Any], *args: Any) -> None: + """Log an exception with additional context.""" + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + + # Do not print the wrapper in the traceback + frames = len(inspect.trace()) - 1 + exc_msg = traceback.format_exc(-frames) + friendly_msg = format_err(*args) + logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg) + + def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any], *args: Any ) -> Callable[[], None]: """Decorate a callback to catch and log exceptions.""" - def log_exception(*args: Any) -> None: - module = inspect.getmodule(inspect.stack()[1][0]) - if module is not None: - module_name = module.__name__ - else: - # If Python is unable to access the sources files, the call stack frame - # will be missing information, so let's guard. - # https://github.com/home-assistant/home-assistant/issues/24982 - module_name = __name__ - - # Do not print the wrapper in the traceback - frames = len(inspect.trace()) - 1 - exc_msg = traceback.format_exc(-frames) - friendly_msg = format_err(*args) - logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg) - # Check for partials to properly determine if coroutine function check_func = func while isinstance(check_func, partial): @@ -159,7 +161,7 @@ def catch_log_exception( try: await func(*args) except Exception: # pylint: disable=broad-except - log_exception(*args) + log_exception(format_err, *args) wrapper_func = async_wrapper else: @@ -170,7 +172,7 @@ def catch_log_exception( try: func(*args) except Exception: # pylint: disable=broad-except - log_exception(*args) + log_exception(format_err, *args) wrapper_func = wrapper return wrapper_func @@ -186,20 +188,7 @@ def catch_log_coro_exception( try: return await target except Exception: # pylint: disable=broad-except - module = inspect.getmodule(inspect.stack()[1][0]) - if module is not None: - module_name = module.__name__ - else: - # If Python is unable to access the sources files, the frame - # will be missing information, so let's guard. - # https://github.com/home-assistant/home-assistant/issues/24982 - module_name = __name__ - - # Do not print the wrapper in the traceback - frames = len(inspect.trace()) - 1 - exc_msg = traceback.format_exc(-frames) - friendly_msg = format_err(*args) - logging.getLogger(module_name).error("%s\n%s", friendly_msg, exc_msg) + log_exception(format_err, *args) return None return coro_wrapper() diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index e4d376dc487..94b43ad7803 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -1,7 +1,9 @@ """Network utilities.""" -from ipaddress import IPv4Address, IPv6Address, ip_network +from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network from typing import Union +import yarl + # RFC6890 - IP addresses of loopback interfaces LOOPBACK_NETWORKS = ( ip_network("127.0.0.0/8"), @@ -39,3 +41,21 @@ def is_link_local(address: Union[IPv4Address, IPv6Address]) -> bool: def is_local(address: Union[IPv4Address, IPv6Address]) -> bool: """Check if an address is loopback or private.""" return is_loopback(address) or is_private(address) + + +def is_ip_address(address: str) -> bool: + """Check if a given string is an IP address.""" + try: + ip_address(address) + except ValueError: + return False + + return True + + +def normalize_url(address: str) -> str: + """Normalize a given URL.""" + url = yarl.URL(address.rstrip("/")) + if url.is_default_port(): + return str(url.with_port(None)) + return str(url) diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py new file mode 100644 index 00000000000..e5654e6f8c6 --- /dev/null +++ b/homeassistant/util/thread.py @@ -0,0 +1,26 @@ +"""Threading util helpers.""" +import sys +import threading +from typing import Any + + +def fix_threading_exception_logging() -> None: + """Fix threads passing uncaught exceptions to our exception hook. + + https://bugs.python.org/issue1230540 + Fixed in Python 3.8. + """ + if sys.version_info[:2] >= (3, 8): + return + + run_old = threading.Thread.run + + def run(*args: Any, **kwargs: Any) -> None: + try: + run_old(*args, **kwargs) + except (KeyboardInterrupt, SystemExit): # pylint: disable=try-except-raise + raise + except Exception: # pylint: disable=broad-except + sys.excepthook(*sys.exc_info()) + + threading.Thread.run = run # type: ignore diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index ffcd4917363..37df6bb89f5 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -24,29 +24,28 @@ def save_yaml(path: str, data: dict) -> None: # From: https://gist.github.com/miracle2k/3184458 -# pylint: disable=redefined-outer-name def represent_odict( # type: ignore - dump, tag, mapping, flow_style=None + dumper, tag, mapping, flow_style=None ) -> yaml.MappingNode: """Like BaseRepresenter.represent_mapping but does not issue the sort().""" value: list = [] node = yaml.MappingNode(tag, value, flow_style=flow_style) - if dump.alias_key is not None: - dump.represented_objects[dump.alias_key] = node + if dumper.alias_key is not None: + dumper.represented_objects[dumper.alias_key] = node best_style = True if hasattr(mapping, "items"): mapping = mapping.items() for item_key, item_value in mapping: - node_key = dump.represent_data(item_key) - node_value = dump.represent_data(item_value) + node_key = dumper.represent_data(item_key) + node_value = dumper.represent_data(item_value) if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): best_style = False if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): best_style = False value.append((node_key, node_value)) if flow_style is None: - if dump.default_flow_style is not None: - node.flow_style = dump.default_flow_style + if dumper.default_flow_style is not None: + node.flow_style = dumper.default_flow_style else: node.flow_style = best_style return node diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 3727a156b97..8d713bf494f 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -88,9 +88,6 @@ def _add_reference( ... -# pylint: enable=pointless-statement - - def _add_reference( # type: ignore obj, loader: SafeLineLoader, node: yaml.nodes.Node ): diff --git a/pylintrc b/pylintrc index a8118d23910..1369c3657d7 100644 --- a/pylintrc +++ b/pylintrc @@ -8,7 +8,7 @@ persistent=no extension-pkg-whitelist=ciso8601 [BASIC] -good-names=id,i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp,T [MESSAGES CONTROL] # Reasons disabled: @@ -18,7 +18,6 @@ good-names=id,i,j,k,ex,Run,_,fp # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # unused-argument - generic callbacks and setup methods create a lot of warnings -# redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing @@ -34,7 +33,6 @@ disable= inconsistent-return-statements, locally-disabled, not-context-manager, - redefined-variable-type, too-few-public-methods, too-many-ancestors, too-many-arguments, diff --git a/requirements_all.txt b/requirements_all.txt index 804299d2e37..f51773815dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,10 +9,10 @@ ciso8601==2.1.3 importlib-metadata==1.6.0 jinja2>=2.11.1 PyJWT==1.7.1 -cryptography==2.9 +cryptography==2.9.2 pip>=8.0.3 python-slugify==4.0.0 -pytz>=2019.03 +pytz>=2020.1 pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.8.2 +HAP-python==2.8.4 # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -46,12 +46,12 @@ OPi.GPIO==0.4.0 # homeassistant.components.essent PyEssent==0.13 +# homeassistant.components.flick_electric +PyFlick==0.0.2 + # homeassistant.components.github PyGithub==1.43.8 -# homeassistant.components.isy994 -PyISY==1.1.2 - # homeassistant.components.mvglive PyMVGLive==1.1.4 @@ -78,6 +78,9 @@ PySocks==1.7.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.homekit +PyTurboJPEG==1.4.0 + # homeassistant.components.vicare PyViCare==0.1.10 @@ -101,6 +104,9 @@ TwitterAPI==2.5.11 # homeassistant.components.tof # VL53L1X2==0.1.5 +# homeassistant.components.onvif +WSDiscovery==2.0.0 + # homeassistant.components.waze_travel_time WazeRouteCalculator==0.12 @@ -128,6 +134,9 @@ adguardhome==0.4.2 # homeassistant.components.frontier_silicon afsapi==0.0.4 +# homeassistant.components.agent_dvr +agent-py==0.0.20 + # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.12 @@ -209,10 +218,10 @@ aiopvpc==1.0.2 aiopylgtv==0.3.3 # homeassistant.components.switcher_kis -aioswitcher==1.1.0 +aioswitcher==1.2.0 # homeassistant.components.unifi -aiounifi==20 +aiounifi==22 # homeassistant.components.wwlln aiowwlln==2.0.2 @@ -227,7 +236,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.3 +alpha_vantage==2.2.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 @@ -260,7 +269,7 @@ aprslib==0.6.46 aqualogic==1.0 # homeassistant.components.arcam_fmj -arcam-fmj==0.4.3 +arcam-fmj==0.4.4 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.0.0 @@ -282,7 +291,7 @@ atenpdu==0.3.0 aurorapy==0.2.6 # homeassistant.components.stream -av==6.1.2 +av==7.0.1 # homeassistant.components.avea avea==1.4 @@ -324,7 +333,7 @@ beautifulsoup4==4.9.0 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.15.2 +bellows==0.16.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.5 @@ -332,8 +341,11 @@ bimmer_connected==0.7.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.blebox +blebox_uniapi==1.3.2 + # homeassistant.components.blink -blinkpy==0.14.3 +blinkpy==0.15.0 # homeassistant.components.blinksticklight blinkstick==1.1.8 @@ -359,10 +371,10 @@ bomradarloop==0.1.4 boto3==1.9.252 # homeassistant.components.braviatv -bravia-tv==1.0.3 +bravia-tv==1.0.4 # homeassistant.components.broadlink -broadlink==0.13.2 +broadlink==0.14.0 # homeassistant.components.brother brother==0.1.14 @@ -373,6 +385,9 @@ brottsplatskartan==0.0.1 # homeassistant.components.brunt brunt==0.1.3 +# homeassistant.components.bsblan +bsblan==0.3.7 + # homeassistant.components.bluetooth_tracker bt_proximity==0.2 @@ -453,6 +468,9 @@ deluge-client==1.7.1 # homeassistant.components.denonavr denonavr==0.8.1 +# homeassistant.components.devolo_home_control +devolo-home-control-api==0.10.0 + # homeassistant.components.directv directv==0.3.0 @@ -481,7 +499,7 @@ dsmr_parser==0.18 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.39 +dynalite_devices==0.1.40 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.4 @@ -520,7 +538,7 @@ env_canada==0.0.35 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.11.0 +envoy_reader==0.16.1 # homeassistant.components.season ephem==3.7.7.0 @@ -674,7 +692,7 @@ ha-ffmpeg==2.0 ha-philipsjs==0.0.8 # homeassistant.components.plugwise -haanna==0.14.3 +haanna==0.15.0 # homeassistant.components.habitica habitipy==0.2.0 @@ -713,11 +731,14 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200427.2 +home-assistant-frontend==20200519.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 +# homeassistant.components.home_connect +homeconnect==0.5 + # homeassistant.components.homematicip_cloud homematicip==0.10.17 @@ -729,7 +750,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.11 +huawei-lte-api==1.4.12 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -816,7 +837,7 @@ libpyvivotek==0.4.0 librouteros==3.0.0 # homeassistant.components.soundtouch -libsoundtouch==0.7.2 +libsoundtouch==0.8 # homeassistant.components.life360 life360==4.1.1 @@ -943,7 +964,7 @@ niko-home-control==0.2.1 niluclient==0.1.2 # homeassistant.components.nederlandse_spoorwegen -nsapi==3.0.3 +nsapi==3.0.4 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -951,11 +972,14 @@ nsw-fuel-api-client==1.0.10 # homeassistant.components.nuheat nuheat==0.3.0 +# homeassistant.components.numato +numato-gpio==0.7.1 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.18.2 +numpy==1.18.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -970,7 +994,10 @@ oemthermostat==1.1 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==0.2.0 +onvif-zeep-async==0.3.0 + +# homeassistant.components.opengarage +open-garage==0.1.4 # homeassistant.components.opencv # opencv-python-headless==4.2.0.32 @@ -1045,19 +1072,19 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==7.1.1 +pillow==7.1.2 # homeassistant.components.dominos pizzapi==0.0.3 # homeassistant.components.plex -plexapi==3.4.0 +plexapi==3.6.0 # homeassistant.components.plex plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.7 +plexwebsocket==0.0.8 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1099,6 +1126,9 @@ ptvsd==4.2.8 # homeassistant.components.wink pubnubsub-handler==1.0.8 +# homeassistant.components.pulseaudio_loopback +pulsectl==20.2.4 + # homeassistant.components.androidtv pure-python-adb==0.2.2.dev0 @@ -1149,7 +1179,7 @@ pyRFXtrx==0.25 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.13.8 +pyTibber==0.14.0 # homeassistant.components.dlink pyW215==0.7.0 @@ -1173,7 +1203,7 @@ pyaehw4a1==0.3.4 pyaftership==0.1.2 # homeassistant.components.airvisual -pyairvisual==3.0.1 +pyairvisual==4.4.0 # homeassistant.components.almond pyalmond==0.0.2 @@ -1182,10 +1212,10 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.19 +pyatag==0.3.1.2 # homeassistant.components.netatmo -pyatmo==3.3.0 +pyatmo==3.3.1 # homeassistant.components.atome pyatome==0.1.1 @@ -1215,7 +1245,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==5.0.0 +pychromecast==5.1.0 # homeassistant.components.cmus pycmus==0.1.1 @@ -1233,7 +1263,7 @@ pycsspeechtts==1.0.3 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.6.3 +pydaikin==2.0.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 @@ -1301,6 +1331,9 @@ pyflunearyou==1.0.7 # homeassistant.components.futurenow pyfnip==0.2 +# homeassistant.components.forked_daapd +pyforked-daapd==0.1.8 + # homeassistant.components.fritzbox pyfritzhome==0.4.2 @@ -1362,6 +1395,9 @@ pyirishrail==0.0.2 # homeassistant.components.iss pyiss==1.0.1 +# homeassistant.components.isy994 +pyisy==2.0.2 + # homeassistant.components.itach pyitachip2ir==0.0.7 @@ -1383,6 +1419,9 @@ pylaunches==0.2.0 # homeassistant.components.lg_netcast pylgnetcast-homeassistant==0.2.0.dev0 +# homeassistant.components.forked_daapd +pylibrespot-java==0.1.0 + # homeassistant.components.linky pylinky==0.4.0 @@ -1405,7 +1444,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.melcloud -pymelcloud==2.4.1 +pymelcloud==2.5.2 # homeassistant.components.somfy pymfy==0.7.1 @@ -1530,7 +1569,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.14 +pysaj==0.0.16 # homeassistant.components.sony_projector pysdcp==1 @@ -1542,7 +1581,8 @@ pysensibo==1.0.3 pyserial-asyncio==0.4 # homeassistant.components.acer_projector -pyserial==3.1.1 +# homeassistant.components.zha +pyserial==3.4 # homeassistant.components.sesame pysesame2==1.0.1 @@ -1551,7 +1591,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.signal_messenger -pysignalclirestapi==0.2.4 +pysignalclirestapi==0.3.4 # homeassistant.components.sma pysma==0.3.5 @@ -1575,11 +1615,14 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.25 +pysonos==0.0.30 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.squeezebox +pysqueezebox==0.1.4 + # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 @@ -1647,7 +1690,7 @@ python-izone==1.1.2 python-join-api==0.0.4 # homeassistant.components.juicenet -python-juicenet==0.1.6 +python-juicenet==1.0.1 # homeassistant.components.lirc # python-lirc==1.2.3 @@ -1659,7 +1702,7 @@ python-miio==0.5.0.1 python-mpd2==1.0.0 # homeassistant.components.mystrom -python-mystrom==0.5.0 +python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.1.0 @@ -1667,6 +1710,9 @@ python-nest==4.1.0 # homeassistant.components.nmap_tracker python-nmap==0.6.1 +# homeassistant.components.ozw +python-openzwave-mqtt==1.0.1 + # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 @@ -1677,10 +1723,10 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11.2 +python-songpal==0.12 # homeassistant.components.synology_dsm -python-synology==0.8.0 +python-synology==0.8.1 # homeassistant.components.tado python-tado==0.8.1 @@ -1756,7 +1802,7 @@ pyvesync==1.1.0 pyvizio==0.1.47 # homeassistant.components.velux -pyvlx==0.2.12 +pyvlx==0.2.14 # homeassistant.components.html5 pywebpush==1.9.2 @@ -1773,6 +1819,9 @@ pyzabbix==0.7.4 # homeassistant.components.qrcode pyzbar==0.1.7 +# homeassistant.components.zerproc +pyzerproc==0.2.4 + # homeassistant.components.qnap qnapstats==0.3.0 @@ -1822,10 +1871,10 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==4.1.0 +rokuecp==0.4.0 # homeassistant.components.roomba -roombapy==1.5.3 +roombapy==1.6.1 # homeassistant.components.rova rova==0.1.0 @@ -1867,7 +1916,7 @@ sendgrid==6.2.1 sense-hat==2.2.0 # homeassistant.components.sense -sense_energy==0.7.1 +sense_energy==0.7.2 # homeassistant.components.sentry sentry-sdk==0.13.5 @@ -1885,7 +1934,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.0.7 +simplisafe-python==9.2.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 @@ -1900,7 +1949,7 @@ slackclient==2.5.0 sleepyq==0.7 # homeassistant.components.xmpp -slixmpp==1.4.2 +slixmpp==1.5.1 # homeassistant.components.smappee smappy==0.2.16 @@ -1953,7 +2002,7 @@ spiderpy==1.3.1 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.11.1 +spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql @@ -1990,7 +2039,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.3 +surepy==0.2.5 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 @@ -2059,7 +2108,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.5 +tuyaha==0.0.6 # homeassistant.components.twentemilieu twentemilieu==0.3.0 @@ -2073,6 +2122,9 @@ uEagle==0.0.1 # homeassistant.components.unifiled unifiled==0.11 +# homeassistant.components.upb +upb_lib==0.4.11 + # homeassistant.components.upcloud upcloud-api==0.4.5 @@ -2092,7 +2144,7 @@ vallox-websocket-api==2.4.0 venstarcolortouch==0.12 # homeassistant.components.meteo_france -vigilancemeteo==3.0.0 +vigilancemeteo==3.0.1 # homeassistant.components.vilfo vilfo-api-client==0.3.2 @@ -2130,6 +2182,9 @@ webexteamssdk==1.1.1 # homeassistant.components.gpmdp websocket-client==0.54.0 +# homeassistant.components.wiffi +wiffi==1.0.0 + # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 @@ -2152,7 +2207,7 @@ xboxapi==0.1.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.11.2 +xknx==0.11.3 # homeassistant.components.bluesound # homeassistant.components.rest @@ -2178,16 +2233,16 @@ yeelight==0.5.1 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.03.24 +youtube_dl==2020.05.08 # homeassistant.components.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.25.1 +zeroconf==0.26.1 # homeassistant.components.zha -zha-quirks==0.0.38 +zha-quirks==0.0.39 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2196,19 +2251,19 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.3.1 +zigpy-cc==0.4.2 # homeassistant.components.zha -zigpy-deconz==0.8.1 +zigpy-deconz==0.9.2 # homeassistant.components.zha -zigpy-homeassistant==0.19.0 +zigpy-xbee==0.12.1 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.11.0 +zigpy-zigate==0.6.1 # homeassistant.components.zha -zigpy-zigate==0.5.1 +zigpy==0.20.4 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test.txt b/requirements_test.txt index 8b4b5d0edcf..54d6f6b036d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,17 +4,18 @@ -r requirements_test_pre_commit.txt asynctest==0.13.0 -codecov==2.0.15 -mock-open==1.3.1 +codecov==2.0.22 +coverage==5.1 +mock-open==1.4.0 mypy==0.770 -pre-commit==2.2.0 +pre-commit==2.4.0 pylint==2.4.4 astroid==2.3.3 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 -pytest-sugar==0.9.2 -pytest-timeout==1.3.3 -pytest==5.3.5 -requests_mock==1.7.0 +pytest-sugar==0.9.3 +pytest-timeout==1.3.4 +pytest==5.4.2 +requests_mock==1.8.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35f0535d77e..b7748649a36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,10 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.8.2 +HAP-python==2.8.4 + +# homeassistant.components.flick_electric +PyFlick==0.0.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks @@ -20,9 +23,15 @@ PyRMVtransport==0.2.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.homekit +PyTurboJPEG==1.4.0 + # homeassistant.components.remember_the_milk RtmAPI==0.7.2 +# homeassistant.components.onvif +WSDiscovery==2.0.0 + # homeassistant.components.yessssms YesssSMS==0.4.1 @@ -35,6 +44,9 @@ adb-shell==0.1.3 # homeassistant.components.adguard adguardhome==0.4.2 +# homeassistant.components.agent_dvr +agent-py==0.0.20 + # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.12 @@ -53,9 +65,6 @@ aioambient==1.1.1 # homeassistant.components.asuswrt aioasuswrt==1.2.5 -# homeassistant.components.automatic -aioautomatic==0.6.5 - # homeassistant.components.aws aiobotocore==0.11.1 @@ -85,6 +94,9 @@ aiohue==2.1.0 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.hunterdouglas_powerview +aiopvapi==1.6.14 + # homeassistant.components.pvpc_hourly_pricing aiopvpc==1.0.2 @@ -92,10 +104,10 @@ aiopvpc==1.0.2 aiopylgtv==0.3.3 # homeassistant.components.switcher_kis -aioswitcher==1.1.0 +aioswitcher==1.2.0 # homeassistant.components.unifi -aiounifi==20 +aiounifi==22 # homeassistant.components.wwlln aiowwlln==2.0.2 @@ -119,14 +131,14 @@ apprise==0.8.5 aprslib==0.6.46 # homeassistant.components.arcam_fmj -arcam-fmj==0.4.3 +arcam-fmj==0.4.4 # homeassistant.components.dlna_dmr # homeassistant.components.upnp async-upnp-client==0.14.13 # homeassistant.components.stream -av==6.1.2 +av==7.0.1 # homeassistant.components.axis axis==25 @@ -135,20 +147,26 @@ axis==25 base36==0.1.1 # homeassistant.components.zha -bellows-homeassistant==0.15.2 +bellows==0.16.2 + +# homeassistant.components.blebox +blebox_uniapi==1.3.2 # homeassistant.components.bom bomradarloop==0.1.4 # homeassistant.components.braviatv -bravia-tv==1.0.3 +bravia-tv==1.0.4 # homeassistant.components.broadlink -broadlink==0.13.2 +broadlink==0.14.0 # homeassistant.components.brother brother==0.1.14 +# homeassistant.components.bsblan +bsblan==0.3.7 + # homeassistant.components.buienradar buienradar==1.0.4 @@ -184,6 +202,9 @@ defusedxml==0.6.0 # homeassistant.components.denonavr denonavr==0.8.1 +# homeassistant.components.devolo_home_control +devolo-home-control-api==0.10.0 + # homeassistant.components.directv directv==0.3.0 @@ -197,7 +218,7 @@ doorbirdpy==2.0.8 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.39 +dynalite_devices==0.1.40 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 @@ -291,11 +312,14 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200427.2 +home-assistant-frontend==20200519.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 +# homeassistant.components.home_connect +homeconnect==0.5 + # homeassistant.components.homematicip_cloud homematicip==0.10.17 @@ -304,7 +328,7 @@ homematicip==0.10.17 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.11 +huawei-lte-api==1.4.12 # homeassistant.components.iaqualink iaqualink==0.3.1 @@ -332,7 +356,7 @@ libpurecool==0.6.1 librouteros==3.0.0 # homeassistant.components.soundtouch -libsoundtouch==0.7.2 +libsoundtouch==0.8 # homeassistant.components.logi_circle logi_circle==0.2.2 @@ -352,6 +376,9 @@ meteofrance==0.3.7 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.mill +millheater==0.3.4 + # homeassistant.components.minio minio==4.0.9 @@ -374,15 +401,21 @@ nsw-fuel-api-client==1.0.10 # homeassistant.components.nuheat nuheat==0.3.0 +# homeassistant.components.numato +numato-gpio==0.7.1 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.18.2 +numpy==1.18.4 # homeassistant.components.google oauth2client==4.0.0 +# homeassistant.components.onvif +onvif-zeep-async==0.3.0 + # homeassistant.components.openerz openerz-api==0.1.0 @@ -408,16 +441,16 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==7.1.1 +pillow==7.1.2 # homeassistant.components.plex -plexapi==3.4.0 +plexapi==3.6.0 # homeassistant.components.plex plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.7 +plexwebsocket==0.0.8 # homeassistant.components.mhz19 # homeassistant.components.serial_pm @@ -463,6 +496,9 @@ pyMetno==0.4.6 # homeassistant.components.rfxtrx pyRFXtrx==0.25 +# homeassistant.components.tibber +pyTibber==0.14.0 + # homeassistant.components.nextbus py_nextbusnext==0.1.4 @@ -470,7 +506,7 @@ py_nextbusnext==0.1.4 pyaehw4a1==0.3.4 # homeassistant.components.airvisual -pyairvisual==3.0.1 +pyairvisual==4.4.0 # homeassistant.components.almond pyalmond==0.0.2 @@ -479,10 +515,10 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.2.19 +pyatag==0.3.1.2 # homeassistant.components.netatmo -pyatmo==3.3.0 +pyatmo==3.3.1 # homeassistant.components.blackbird pyblackbird==0.5 @@ -491,13 +527,13 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==5.0.0 +pychromecast==5.1.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==1.6.3 +pydaikin==2.0.2 # homeassistant.components.deconz pydeconz==70 @@ -517,6 +553,9 @@ pyflume==0.4.0 # homeassistant.components.flunearyou pyflunearyou==1.0.7 +# homeassistant.components.forked_daapd +pyforked-daapd==0.1.8 + # homeassistant.components.fritzbox pyfritzhome==0.4.2 @@ -548,23 +587,32 @@ pyipp==0.10.1 # homeassistant.components.iqvia pyiqvia==0.2.1 +# homeassistant.components.isy994 +pyisy==2.0.2 + # homeassistant.components.kira pykira==0.1.1 # homeassistant.components.lastfm pylast==3.2.1 +# homeassistant.components.forked_daapd +pylibrespot-java==0.1.0 + # homeassistant.components.linky pylinky==0.4.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.lutron_caseta +pylutron-caseta==0.6.1 + # homeassistant.components.mailgun pymailgunner==1.4 # homeassistant.components.melcloud -pymelcloud==2.4.1 +pymelcloud==2.5.2 # homeassistant.components.somfy pymfy==0.7.1 @@ -613,8 +661,12 @@ pyps4-2ndscreen==1.0.7 # homeassistant.components.qwikswitch pyqwikswitch==0.93 +# homeassistant.components.acer_projector +# homeassistant.components.zha +pyserial==3.4 + # homeassistant.components.signal_messenger -pysignalclirestapi==0.2.4 +pysignalclirestapi==0.3.4 # homeassistant.components.sma pysma==0.3.5 @@ -629,7 +681,7 @@ pysmartthings==0.7.1 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.25 +pysonos==0.0.30 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -643,14 +695,23 @@ python-forecastio==1.4.0 # homeassistant.components.izone python-izone==1.1.2 +# homeassistant.components.juicenet +python-juicenet==1.0.1 + # homeassistant.components.xiaomi_miio python-miio==0.5.0.1 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.ozw +python-openzwave-mqtt==1.0.1 + +# homeassistant.components.songpal +python-songpal==0.12 + # homeassistant.components.synology_dsm -python-synology==0.8.0 +python-synology==0.8.1 # homeassistant.components.tado python-tado==0.8.1 @@ -682,6 +743,9 @@ pyvizio==0.1.47 # homeassistant.components.html5 pywebpush==1.9.2 +# homeassistant.components.zerproc +pyzerproc==0.2.4 + # homeassistant.components.rachio rachiopy==0.1.3 @@ -698,10 +762,10 @@ rflink==0.0.52 ring_doorbell==0.6.0 # homeassistant.components.roku -roku==4.1.0 +rokuecp==0.4.0 # homeassistant.components.roomba -roombapy==1.5.3 +roombapy==1.6.1 # homeassistant.components.yamaha rxv==0.6.0 @@ -713,7 +777,7 @@ samsungctl[websocket]==0.7.1 samsungtvws[websocket]==1.4.0 # homeassistant.components.sense -sense_energy==0.7.1 +sense_energy==0.7.2 # homeassistant.components.sentry sentry-sdk==0.13.5 @@ -722,7 +786,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.0.7 +simplisafe-python==9.2.0 # homeassistant.components.sleepiq sleepyq==0.7 @@ -740,7 +804,7 @@ somecomfort==0.5.2 speak2mary==1.4.0 # homeassistant.components.spotify -spotipy==2.11.1 +spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql @@ -779,12 +843,18 @@ total_connect_client==0.54.1 # homeassistant.components.transmission transmissionrpc==0.11 +# homeassistant.components.tuya +tuyaha==0.0.6 + # homeassistant.components.twentemilieu twentemilieu==0.3.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.upb +upb_lib==0.4.11 + # homeassistant.components.huawei_lte url-normalize==1.4.1 @@ -792,7 +862,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.meteo_france -vigilancemeteo==3.0.0 +vigilancemeteo==3.0.1 # homeassistant.components.vilfo vilfo-api-client==0.3.2 @@ -809,6 +879,9 @@ wakeonlan==1.1.6 # homeassistant.components.folder_watcher watchdog==0.8.3 +# homeassistant.components.wiffi +wiffi==1.0.0 + # homeassistant.components.withings withings-api==2.1.3 @@ -827,22 +900,22 @@ xmltodict==0.12.0 ya_ma==0.3.8 # homeassistant.components.zeroconf -zeroconf==0.25.1 +zeroconf==0.26.1 # homeassistant.components.zha -zha-quirks==0.0.38 +zha-quirks==0.0.39 # homeassistant.components.zha -zigpy-cc==0.3.1 +zigpy-cc==0.4.2 # homeassistant.components.zha -zigpy-deconz==0.8.1 +zigpy-deconz==0.9.2 # homeassistant.components.zha -zigpy-homeassistant==0.19.0 +zigpy-xbee==0.12.1 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.11.0 +zigpy-zigate==0.6.1 # homeassistant.components.zha -zigpy-zigate==0.5.1 +zigpy==0.20.4 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a52927ed497..798377780ff 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,5 +7,5 @@ flake8-docstrings==1.5.0 flake8==3.7.9 isort==4.3.21 pydocstyle==5.0.2 -pyupgrade==2.1.0 +pyupgrade==2.3.0 yamllint==1.23.0 diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 6971cc28fc9..44f2b2d59ae 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -2,6 +2,8 @@ import json from typing import Dict +from homeassistant.requirements import DISCOVERY_INTEGRATIONS + from .model import Config, Integration BASE = """ @@ -15,33 +17,50 @@ To update, run python3 -m script.hassfest FLOWS = {} """.strip() +UNIQUE_ID_IGNORE = {"esphome", "fritzbox", "heos", "huawei_lte"} -def validate_integration(integration: Integration): - """Validate we can load config flow without installing requirements.""" - if not (integration.path / "config_flow.py").is_file(): + +def validate_integration(config: Config, integration: Integration): + """Validate config flow of an integration.""" + config_flow_file = integration.path / "config_flow.py" + + if not config_flow_file.is_file(): integration.add_error( "config_flow", "Config flows need to be defined in the file config_flow.py" ) + return - # Currently not require being able to load config flow without - # installing requirements. - # try: - # integration.import_pkg('config_flow') - # except ImportError as err: - # integration.add_error( - # 'config_flow', - # "Unable to import config flow: {}. Config flows should be able " - # "to be imported without installing requirements.".format(err)) - # return + needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and any( + bool(integration.manifest.get(key)) + for keys in DISCOVERY_INTEGRATIONS.values() + for key in keys + ) - # if integration.domain not in config_entries.HANDLERS: - # integration.add_error( - # 'config_flow', - # "Importing the config flow platform did not register a config " - # "flow handler.") + if not needs_unique_id: + return + + config_flow = config_flow_file.read_text() + + has_unique_id = ( + "self.async_set_unique_id" in config_flow + or "config_entry_flow.register_discovery_flow" in config_flow + or "config_entry_oauth2_flow.AbstractOAuth2FlowHandler" in config_flow + ) + + if has_unique_id: + return + + if config.specific_integrations: + notice_method = integration.add_warning + else: + notice_method = integration.add_error + + notice_method( + "config_flow", "Config flows that are discoverable need to set a unique ID" + ) -def generate_and_validate(integrations: Dict[str, Integration]): +def generate_and_validate(integrations: Dict[str, Integration], config: Config): """Validate and generate config flow data.""" domains = [] @@ -56,7 +75,7 @@ def generate_and_validate(integrations: Dict[str, Integration]): if not config_flow: continue - validate_integration(integration) + validate_integration(config, integration) domains.append(domain) @@ -66,7 +85,7 @@ def generate_and_validate(integrations: Dict[str, Integration]): def validate(integrations: Dict[str, Integration], config: Config): """Validate config flow file.""" config_flow_path = config.root / "homeassistant/generated/config_flows.py" - config.cache["config_flow"] = content = generate_and_validate(integrations) + config.cache["config_flow"] = content = generate_and_validate(integrations, config) if config.specific_integrations: return diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 7ae2ae818a5..cb592b63b53 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -21,7 +21,7 @@ def documentation_url(value: str) -> str: return value parsed_url = urlparse(value) - if not parsed_url.scheme == DOCUMENTATION_URL_SCHEMA: + if parsed_url.scheme != DOCUMENTATION_URL_SCHEMA: raise vol.Invalid("Documentation url is not prefixed with https") if parsed_url.netloc == DOCUMENTATION_URL_HOST and not parsed_url.path.startswith( DOCUMENTATION_URL_PATH_PREFIX @@ -46,13 +46,14 @@ MANIFEST_SCHEMA = vol.Schema( vol.Required("documentation"): vol.All( vol.Url(), documentation_url # pylint: disable=no-value-for-parameter ), + vol.Optional( + "issue_tracker" + ): vol.Url(), # pylint: disable=no-value-for-parameter vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Optional("requirements"): [str], vol.Optional("dependencies"): [str], vol.Optional("after_dependencies"): [str], vol.Required("codeowners"): [str], - vol.Optional("logo"): vol.Url(), # pylint: disable=no-value-for-parameter - vol.Optional("icon"): vol.Url(), # pylint: disable=no-value-for-parameter } ) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 4606482d0cb..416bfbdb47e 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -91,20 +91,20 @@ def gen_data_entry_schema( """Generate a data entry schema.""" step_title_class = vol.Required if require_step_title else vol.Optional schema = { - vol.Optional("flow_title"): str, + vol.Optional("flow_title"): cv.string_with_no_html, vol.Required("step"): { str: { - step_title_class("title"): str, - vol.Optional("description"): str, - vol.Optional("data"): {str: str}, + step_title_class("title"): cv.string_with_no_html, + vol.Optional("description"): cv.string_with_no_html, + vol.Optional("data"): {str: cv.string_with_no_html}, } }, - vol.Optional("error"): {str: str}, - vol.Optional("abort"): {str: str}, - vol.Optional("create_entry"): {str: str}, + vol.Optional("error"): {str: cv.string_with_no_html}, + vol.Optional("abort"): {str: cv.string_with_no_html}, + vol.Optional("create_entry"): {str: cv.string_with_no_html}, } if flow_title == REQUIRED: - schema[vol.Required("title")] = str + schema[vol.Required("title")] = cv.string_with_no_html elif flow_title == REMOVED: schema[vol.Optional("title", msg=REMOVED_TITLE_MSG)] = partial( removed_title_validator, config, integration @@ -117,7 +117,7 @@ def gen_strings_schema(config: Config, integration: Integration): """Generate a strings schema.""" return vol.Schema( { - vol.Optional("title"): str, + vol.Optional("title"): cv.string_with_no_html, vol.Optional("config"): gen_data_entry_schema( config=config, integration=integration, @@ -131,10 +131,10 @@ def gen_strings_schema(config: Config, integration: Integration): require_step_title=False, ), vol.Optional("device_automation"): { - vol.Optional("action_type"): {str: str}, - vol.Optional("condition_type"): {str: str}, - vol.Optional("trigger_type"): {str: str}, - vol.Optional("trigger_subtype"): {str: str}, + vol.Optional("action_type"): {str: cv.string_with_no_html}, + vol.Optional("condition_type"): {str: cv.string_with_no_html}, + vol.Optional("trigger_type"): {str: cv.string_with_no_html}, + vol.Optional("trigger_subtype"): {str: cv.string_with_no_html}, }, vol.Optional("state"): cv.schema_with_slug_keys( cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), @@ -180,7 +180,7 @@ def gen_platform_strings_schema(config: Config, integration: Integration): """ if not value.startswith(f"{integration.domain}__"): raise vol.Invalid( - f"Device class need to start with '{integration.domain}__'. Key {value} is invalid" + f"Device class need to start with '{integration.domain}__'. Key {value} is invalid. See https://developers.home-assistant.io/docs/internationalization/core#stringssensorjson" ) slug_friendly = value.replace("__", "_", 1) @@ -203,7 +203,7 @@ def gen_platform_strings_schema(config: Config, integration: Integration): ) -ONBOARDING_SCHEMA = vol.Schema({vol.Required("area"): {str: str}}) +ONBOARDING_SCHEMA = vol.Schema({vol.Required("area"): {str: cv.string_with_no_html}}) def validate_translation_file(config: Config, integration: Integration, all_strings): diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 8186b857e80..f4416a7b7e8 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -69,7 +69,7 @@ def print_relevant_docs(template: str, info: Info) -> None: print() print( - f"The next step is to look at the files and deal with all areas marked as TODO." + "The next step is to look at the files and deal with all areas marked as TODO." ) if "extra" in data: diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 4317618ed52..292ae86e3a9 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -116,14 +116,22 @@ def _custom_tasks(template, info) -> None: title=info.name, config={ "step": { - "user": {"title": "Connect to the device", "data": {"host": "Host"}} + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + }, + } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, - "abort": {"already_configured": "Device is already configured"}, }, ) @@ -133,11 +141,13 @@ def _custom_tasks(template, info) -> None: title=info.name, config={ "step": { - "confirm": {"description": f"Do you want to set up {info.name}?"} + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]", + } }, "abort": { - "single_instance_allowed": f"Only a single configuration of {info.name} is possible.", - "no_devices_found": f"No {info.name} devices found on the network.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", }, }, ) @@ -148,22 +158,16 @@ def _custom_tasks(template, info) -> None: title=info.name, config={ "step": { - "pick_implementation": {"title": "Pick Authentication Method"} + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } }, "abort": { - "missing_configuration": "The {info.name} component is not configured. Please follow the documentation." + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", }, "create_entry": { - "default": f"Successfully authenticated with {info.name}." + "default": "[%key:common::config_flow::create_entry::authenticated%]" }, }, ) - _append( - info.integration_dir / "const.py", - """ - -# TODO Update with your own urls -OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize" -OAUTH2_TOKEN = "https://www.example.com/auth/token" -""", - ) diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index 3d829b5cc32..d6dee8d0bd0 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,10 +1,10 @@ """Test the NEW_NAME config flow.""" -from asynctest import patch - from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from tests.async_mock import patch + async def test_form(hass): """Test we get the form.""" diff --git a/script/scaffold/templates/config_flow_oauth2/integration/const.py b/script/scaffold/templates/config_flow_oauth2/integration/const.py new file mode 100644 index 00000000000..7255de72d84 --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/const.py @@ -0,0 +1,7 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" + +# TODO Update with your own urls +OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize" +OAUTH2_TOKEN = "https://www.example.com/auth/token" diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index 8a543a04af3..7dda6564507 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,6 +1,4 @@ """Test the NEW_NAME config flow.""" -from asynctest import patch - from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, @@ -9,6 +7,8 @@ from homeassistant.components.NEW_DOMAIN.const import ( ) from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch + CLIENT_ID = "1234" CLIENT_SECRET = "5678" diff --git a/script/translations/download.py b/script/translations/download.py index 0e8c0664ecb..364f309b644 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """Merge all translation sources into a single JSON file.""" -import glob import json import os import pathlib @@ -47,16 +46,6 @@ def run_download_docker(): raise ExitApp("Failed to download translations") -def load_json(filename: str) -> Union[List, Dict]: - """Load JSON data from a file and return as dict or list. - - Defaults to returning empty dict if file is not found. - """ - with open(filename, encoding="utf-8") as fdesc: - return json.loads(fdesc.read()) - return {} - - def save_json(filename: str, data: Union[List, Dict]): """Save JSON data to a file. @@ -69,11 +58,6 @@ def save_json(filename: str, data: Union[List, Dict]): return False -def get_language(path): - """Get the language code for the given file path.""" - return os.path.splitext(os.path.basename(path))[0] - - def get_component_path(lang, component): """Get the component translation path.""" if os.path.isdir(os.path.join("homeassistant", "components", component)): @@ -132,10 +116,9 @@ def save_language_translations(lang, translations): def write_integration_translations(): """Write integration translations.""" - paths = glob.iglob("build/translations-download/*.json") - for path in paths: - lang = get_language(path) - translations = load_json(path) + for lang_file in DOWNLOAD_DIR.glob("*.json"): + lang = lang_file.stem + translations = json.loads(lang_file.read_text()) save_language_translations(lang, translations) diff --git a/script/translations/frontend.py b/script/translations/frontend.py new file mode 100644 index 00000000000..c955c240478 --- /dev/null +++ b/script/translations/frontend.py @@ -0,0 +1,46 @@ +"""Write updated translations to the frontend.""" +import argparse +import json + +from .const import FRONTEND_DIR +from .download import DOWNLOAD_DIR, run_download_docker +from .util import get_base_arg_parser + +FRONTEND_BACKEND_TRANSLATIONS = FRONTEND_DIR / "translations/backend" + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = get_base_arg_parser() + parser.add_argument( + "--skip-download", action="store_true", help="Skip downloading translations." + ) + return parser.parse_args() + + +def run(): + """Update frontend translations with backend data. + + We use the downloaded Docker files because it gives us each language in 1 file. + """ + args = get_arguments() + + if not args.skip_download: + run_download_docker() + + for lang_file in DOWNLOAD_DIR.glob("*.json"): + translations = json.loads(lang_file.read_text()) + + to_write_translations = {"component": {}} + + for domain, domain_translations in translations["component"].items(): + if "state" not in domain_translations: + continue + + to_write_translations["component"][domain] = { + "state": domain_translations["state"] + } + + (FRONTEND_BACKEND_TRANSLATIONS / lang_file.name).write_text( + json.dumps(to_write_translations, indent=2) + ) diff --git a/script/translations/migrate.py b/script/translations/migrate.py index fcf44e3dece..c4c47600698 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -324,30 +324,63 @@ def find_frontend_states(): migrate_project_keys_translations(FRONTEND_PROJECT_ID, CORE_PROJECT_ID, to_migrate) +def apply_data_references(to_migrate): + """Apply references.""" + for strings_file in INTEGRATIONS_DIR.glob("*/strings.json"): + strings = json.loads(strings_file.read_text()) + steps = strings.get("config", {}).get("step") + + if not steps: + continue + + changed = False + + for step_data in steps.values(): + step_data = step_data.get("data", {}) + for key, value in step_data.items(): + + if key in to_migrate and value != to_migrate[key]: + if key.split("_")[0].lower() in value.lower(): + step_data[key] = to_migrate[key] + changed = True + elif value.startswith("[%key"): + pass + else: + print( + f"{strings_file}: Skipped swapping '{key}': '{value}' does not contain '{key}'" + ) + + if not changed: + continue + + strings_file.write_text(json.dumps(strings, indent=2)) + + def run(): """Migrate translations.""" - # Import new common keys - # migrate_project_keys_translations( - # FRONTEND_PROJECT_ID, + apply_data_references( + { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "usb_path": "[%key:common::config_flow::data::usb_path%]", + "access_token": "[%key:common::config_flow::data::access_token%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + } + ) + + # Rename existing keys to common keys, + # Old keys have been updated with reference to the common key + # rename_keys( # CORE_PROJECT_ID, # { - # "state::default::off": "common::state::off", - # "state::default::on": "common::state::on", - # "state::cover::open": "common::state::open", - # "state::cover::closed": "common::state::closed", - # "state::binary_sensor::connectivity::on": "common::state::connected", - # "state::binary_sensor::connectivity::off": "common::state::disconnected", - # "state::lock::locked": "common::state::locked", - # "state::lock::unlocked": "common::state::unlocked", - # "state::timer::active": "common::state::active", - # "state::camera::idle": "common::state::idle", - # "state::media_player::standby": "common::state::standby", - # "state::media_player::paused": "common::state::paused", - # "state::device_tracker::home": "common::state::home", - # "state::device_tracker::not_home": "common::state::not_home", + # "component::blebox::config::step::user::data::host": "common::config_flow::data::ip", # }, # ) - find_frontend_states() + # find_frontend_states() + + # find_different_languages() return 0 diff --git a/script/translations/util.py b/script/translations/util.py index 2cc7dfff689..9839fefd9d5 100644 --- a/script/translations/util.py +++ b/script/translations/util.py @@ -13,7 +13,7 @@ def get_base_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "action", type=str, - choices=["clean", "develop", "download", "migrate", "upload"], + choices=["clean", "develop", "download", "frontend", "migrate", "upload"], ) parser.add_argument("--debug", action="store_true", help="Enable log output") return parser diff --git a/setup.py b/setup.py index 0c56e89b67c..1473fd1f5f9 100755 --- a/setup.py +++ b/setup.py @@ -43,10 +43,10 @@ REQUIRES = [ "jinja2>=2.11.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==2.9", + "cryptography==2.9.2", "pip>=8.0.3", "python-slugify==4.0.0", - "pytz>=2019.03", + "pytz>=2020.1", "pyyaml==5.3.1", "requests==2.23.0", "ruamel.yaml==0.15.100", diff --git a/tests/async_mock.py b/tests/async_mock.py new file mode 100644 index 00000000000..1942b2ca284 --- /dev/null +++ b/tests/async_mock.py @@ -0,0 +1,8 @@ +"""Mock utilities that are async aware.""" +import sys + +if sys.version_info[:2] < (3, 8): + from asynctest.mock import * # noqa + from asynctest.mock import CoroutineMock as AsyncMock # noqa +else: + from unittest.mock import * # noqa diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index c79d76baf4f..65ee5d5d0c5 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,12 +1,12 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" import asyncio -from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA +from tests.async_mock import patch from tests.common import MockUser, async_mock_service MOCK_CODE = "123456" diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index d0a4f3cf3ac..b14b20297eb 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,11 +1,11 @@ """Test the Time-based One Time Password (MFA) auth module.""" import asyncio -from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config +from tests.async_mock import patch from tests.common import MockUser MOCK_CODE = "123456" diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index abcf124b9c4..3915950cedb 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -1,7 +1,6 @@ """Tests for the command_line auth provider.""" import os -from unittest.mock import Mock import uuid import pytest @@ -11,7 +10,7 @@ from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE -from tests.common import mock_coro +from tests.async_mock import AsyncMock @pytest.fixture @@ -63,7 +62,7 @@ async def test_match_existing_credentials(store, provider): data={"username": "good-user"}, is_new=False, ) - provider.async_credentials = Mock(return_value=mock_coro([existing])) + provider.async_credentials = AsyncMock(return_value=[existing]) credentials = await provider.async_get_or_create_credentials( {"username": "good-user", "password": "irrelevant"} ) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 9a275c78ba6..f9ac8fbb92a 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,6 +1,5 @@ """Test the Home Assistant local auth provider.""" import asyncio -from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -12,7 +11,7 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) -from tests.common import mock_coro +from tests.async_mock import Mock, patch @pytest.fixture @@ -156,9 +155,7 @@ async def test_get_or_create_credentials(hass, data): provider = manager.auth_providers[0] provider.data = data credentials1 = await provider.async_get_or_create_credentials({"username": "hello"}) - with patch.object( - provider, "async_credentials", return_value=mock_coro([credentials1]) - ): + with patch.object(provider, "async_credentials", return_value=[credentials1]): credentials2 = await provider.async_get_or_create_credentials( {"username": "hello "} ) @@ -264,17 +261,13 @@ async def test_legacy_get_or_create_credentials(hass, legacy_data): provider.data = legacy_data credentials1 = await provider.async_get_or_create_credentials({"username": "hello"}) - with patch.object( - provider, "async_credentials", return_value=mock_coro([credentials1]) - ): + with patch.object(provider, "async_credentials", return_value=[credentials1]): credentials2 = await provider.async_get_or_create_credentials( {"username": "hello"} ) assert credentials1 is credentials2 - with patch.object( - provider, "async_credentials", return_value=mock_coro([credentials1]) - ): + with patch.object(provider, "async_credentials", return_value=[credentials1]): credentials3 = await provider.async_get_or_create_credentials( {"username": "hello "} ) diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index c5b3a8db038..c2b16cbafab 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -1,5 +1,4 @@ """Tests for the insecure example auth provider.""" -from unittest.mock import Mock import uuid import pytest @@ -7,7 +6,7 @@ import pytest from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example -from tests.common import mock_coro +from tests.async_mock import AsyncMock @pytest.fixture @@ -63,7 +62,7 @@ async def test_match_existing_credentials(store, provider): data={"username": "user-test"}, is_new=False, ) - provider.async_credentials = Mock(return_value=mock_coro([existing])) + provider.async_credentials = AsyncMock(return_value=[existing]) credentials = await provider.async_get_or_create_credentials( {"username": "user-test", "password": "password-test"} ) diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 109eb20fb6c..78ab9829ab6 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -1,10 +1,10 @@ """Tests for the auth store.""" import asyncio -import asynctest - from homeassistant.auth import auth_store +from tests.async_mock import patch + async def test_loading_no_group_data_format(hass, hass_storage): """Test we correctly load old data without any groups.""" @@ -229,12 +229,12 @@ async def test_system_groups_store_id_and_name(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" store = auth_store.AuthStore(hass) - with asynctest.patch( + with patch( "homeassistant.helpers.entity_registry.async_get_registry" - ) as mock_ent_registry, asynctest.patch( + ) as mock_ent_registry, patch( "homeassistant.helpers.device_registry.async_get_registry" - ) as mock_dev_registry, asynctest.patch( - "homeassistant.helpers.storage.Store.async_load" + ) as mock_dev_registry, patch( + "homeassistant.helpers.storage.Store.async_load", return_value=None ) as mock_load: results = await asyncio.gather(store.async_get_users(), store.async_get_users()) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index edcd01d51e1..f303a59179b 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -1,6 +1,5 @@ """Tests for the Home Assistant auth module.""" from datetime import timedelta -from unittest.mock import Mock, patch import jwt import pytest @@ -12,6 +11,7 @@ from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util +from tests.async_mock import Mock, patch from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store diff --git a/tests/common.py b/tests/common.py index f39d458bbe0..d6ae25adb5e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -11,7 +11,6 @@ import logging import os import sys import threading -from unittest.mock import MagicMock, Mock, patch import uuid from aiohttp.test_utils import unused_port as get_test_instance_port # noqa @@ -60,6 +59,8 @@ import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader +from tests.async_mock import AsyncMock, MagicMock, Mock, patch + _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" @@ -159,20 +160,37 @@ async def async_test_home_assistant(loop): def async_add_job(target, *args): """Add job.""" - if isinstance(target, Mock): - return mock_coro(target(*args)) + check_target = target + while isinstance(check_target, ft.partial): + check_target = check_target.func + + if isinstance(check_target, Mock) and not isinstance(target, AsyncMock): + fut = asyncio.Future() + fut.set_result(target(*args)) + return fut + return orig_async_add_job(target, *args) def async_add_executor_job(target, *args): """Add executor job.""" - if isinstance(target, Mock): - return mock_coro(target(*args)) + check_target = target + while isinstance(check_target, ft.partial): + check_target = check_target.func + + if isinstance(check_target, Mock): + fut = asyncio.Future() + fut.set_result(target(*args)) + return fut + return orig_async_add_executor_job(target, *args) def async_create_task(coroutine): """Create task.""" - if isinstance(coroutine, Mock): - return mock_coro() + if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): + fut = asyncio.Future() + fut.set_result(None) + return fut + return orig_async_create_task(coroutine) hass.async_add_job = async_add_job @@ -311,15 +329,16 @@ async def async_mock_mqtt_component(hass, config=None): if config is None: config = {mqtt.CONF_BROKER: "mock-broker"} - async def _async_fire_mqtt_message(topic, payload, qos, retain): + @ha.callback + def _async_fire_mqtt_message(topic, payload, qos, retain): async_fire_mqtt_message(hass, topic, payload, qos, retain) with patch("paho.mqtt.client.Client") as mock_client: - mock_client().connect.return_value = 0 - mock_client().subscribe.return_value = (0, 0) - mock_client().unsubscribe.return_value = (0, 0) - mock_client().publish.return_value = (0, 0) - mock_client().publish.side_effect = _async_fire_mqtt_message + mock_client = mock_client.return_value + mock_client.connect.return_value = 0 + mock_client.subscribe.return_value = (0, 0) + mock_client.unsubscribe.return_value = (0, 0) + mock_client.publish.side_effect = _async_fire_mqtt_message result = await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) assert result @@ -503,7 +522,7 @@ class MockModule: self.async_setup = async_setup if setup is None and async_setup is None: - self.async_setup = mock_coro_func(True) + self.async_setup = AsyncMock(return_value=True) if async_setup_entry is not None: self.async_setup_entry = async_setup_entry @@ -561,7 +580,7 @@ class MockPlatform: self.async_setup_entry = async_setup_entry if setup_platform is None and async_setup_platform is None: - self.async_setup_platform = mock_coro_func() + self.async_setup_platform = AsyncMock(return_value=None) class MockEntityPlatform(entity_platform.EntityPlatform): @@ -725,20 +744,12 @@ def patch_yaml_files(files_dict, endswith=True): def mock_coro(return_value=None, exception=None): """Return a coro that returns a value or raise an exception.""" - return mock_coro_func(return_value, exception)() - - -def mock_coro_func(return_value=None, exception=None): - """Return a method to create a coro function that returns a value.""" - - @asyncio.coroutine - def coro(*args, **kwargs): - """Fake coroutine.""" - if exception: - raise exception - return return_value - - return coro + fut = asyncio.Future() + if exception is not None: + fut.set_exception(exception) + else: + fut.set_result(return_value) + return fut @contextmanager @@ -823,52 +834,6 @@ def mock_restore_cache(hass, states): hass.data[key] = hass.async_create_task(get_restore_state_data()) -class MockDependency: - """Decorator to mock install a dependency.""" - - def __init__(self, root, *args): - """Initialize decorator.""" - self.root = root - self.submodules = args - - def __enter__(self): - """Start mocking.""" - - def resolve(mock, path): - """Resolve a mock.""" - if not path: - return mock - - return resolve(getattr(mock, path[0]), path[1:]) - - base = MagicMock() - to_mock = { - f"{self.root}.{tom}": resolve(base, tom.split(".")) - for tom in self.submodules - } - to_mock[self.root] = base - - self.patcher = patch.dict("sys.modules", to_mock) - self.patcher.start() - return base - - def __exit__(self, *exc): - """Stop mocking.""" - self.patcher.stop() - return False - - def __call__(self, func): - """Apply decorator.""" - - def run_mocked(*args, **kwargs): - """Run with mocked dependencies.""" - with self as base: - args = list(args) + [base] - func(*args, **kwargs) - - return run_mocked - - class MockEntity(entity.Entity): """Mock Entity class.""" diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index aabc732daa2..157a5441bb1 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -1,10 +1,9 @@ """Common methods used across tests for Abode.""" -from unittest.mock import patch - from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index ca546157c93..d64b211f304 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,6 +1,4 @@ """Tests for the Abode alarm control panel device.""" -from unittest.mock import PropertyMock, patch - import abodepy.helpers.constants as CONST from homeassistant.components.abode import ATTR_DEVICE_ID @@ -19,6 +17,8 @@ from homeassistant.const import ( from .common import setup_platform +from tests.async_mock import PropertyMock, patch + DEVICE_ID = "alarm_control_panel.abode_alarm" diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index 8b11671a456..0e843c59023 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -1,12 +1,12 @@ """Tests for the Abode camera device.""" -from unittest.mock import patch - from homeassistant.components.abode.const import DOMAIN as ABODE_DOMAIN from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_IDLE from .common import setup_platform +from tests.async_mock import patch + async def test_entity_registry(hass): """Tests that the devices are registered in the entity registry.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index d9762296e70..01508d412a2 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for the Abode config flow.""" -from unittest.mock import patch - from abodepy.exceptions import AbodeAuthenticationException from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERVER_ERROR +from tests.async_mock import patch from tests.common import MockConfigEntry CONF_POLLING = "polling" diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index bb1b8fceffb..b166ec5464a 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -1,6 +1,4 @@ """Tests for the Abode cover device.""" -from unittest.mock import patch - from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.const import ( @@ -13,6 +11,8 @@ from homeassistant.const import ( from .common import setup_platform +from tests.async_mock import patch + DEVICE_ID = "cover.garage_door" diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 3f73ccd77ce..1598e7bfa91 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,6 +1,4 @@ """Tests for the Abode module.""" -from unittest.mock import patch - from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -11,6 +9,8 @@ from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from .common import setup_platform +from tests.async_mock import patch + async def test_change_settings(hass): """Test change_setting service.""" diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index f0eee4b209b..6506746783c 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -1,6 +1,4 @@ """Tests for the Abode light device.""" -from unittest.mock import patch - from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -19,6 +17,8 @@ from homeassistant.const import ( from .common import setup_platform +from tests.async_mock import patch + DEVICE_ID = "light.living_room_lamp" diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index 45e17861d33..6850eebe0ce 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -1,6 +1,4 @@ """Tests for the Abode lock device.""" -from unittest.mock import patch - from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( @@ -13,6 +11,8 @@ from homeassistant.const import ( from .common import setup_platform +from tests.async_mock import patch + DEVICE_ID = "lock.test_lock" diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index 3ec9648d87d..5c480b33225 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -1,6 +1,4 @@ """Tests for the Abode switch device.""" -from unittest.mock import patch - from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION, @@ -16,6 +14,8 @@ from homeassistant.const import ( from .common import setup_platform +from tests.async_mock import patch + AUTOMATION_ID = "switch.test_automation" AUTOMATION_UID = "47fae27488f74f55b964a81a066c3a01" DEVICE_ID = "switch.test_switch" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index a0d575deac0..d0e874bacdc 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the AdGuard Home config flow.""" -from unittest.mock import patch import aiohttp @@ -15,7 +14,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { CONF_HOST: "127.0.0.1", @@ -156,22 +156,16 @@ async def test_hassio_update_instance_running(hass, aioclient_mock): entry.add_to_hass(hass) with patch.object( - hass.config_entries, - "async_forward_entry_setup", - side_effect=lambda *_: mock_coro(True), + hass.config_entries, "async_forward_entry_setup", return_value=True, ) as mock_load: assert await hass.config_entries.async_setup(entry.entry_id) assert entry.state == config_entries.ENTRY_STATE_LOADED assert len(mock_load.mock_calls) == 2 with patch.object( - hass.config_entries, - "async_forward_entry_unload", - side_effect=lambda *_: mock_coro(True), + hass.config_entries, "async_forward_entry_unload", return_value=True, ) as mock_unload, patch.object( - hass.config_entries, - "async_forward_entry_setup", - side_effect=lambda *_: mock_coro(True), + hass.config_entries, "async_forward_entry_setup", return_value=True, ) as mock_load: result = await hass.config_entries.flow.async_init( "adguard", diff --git a/tests/components/agent_dvr/__init__.py b/tests/components/agent_dvr/__init__.py new file mode 100644 index 00000000000..f0c059d12e2 --- /dev/null +++ b/tests/components/agent_dvr/__init__.py @@ -0,0 +1,42 @@ +"""Tests for the agent_dvr component.""" + +from homeassistant.components.agent_dvr.const import DOMAIN, SERVER_URL +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def init_integration( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the Agent DVR integration in Home Assistant.""" + + aioclient_mock.get( + "http://example.local:8090/command.cgi?cmd=getStatus", + text=load_fixture("agent_dvr/status.json"), + headers={"Content-Type": "application/json"}, + ) + aioclient_mock.get( + "http://example.local:8090/command.cgi?cmd=getObjects", + text=load_fixture("agent_dvr/objects.json"), + headers={"Content-Type": "application/json"}, + ) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="c0715bba-c2d0-48ef-9e3e-bc81c9ea4447", + data={ + CONF_HOST: "example.local", + CONF_PORT: 8090, + SERVER_URL: "http://example.local:8090/", + }, + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py new file mode 100644 index 00000000000..33b5805700f --- /dev/null +++ b/tests/components/agent_dvr/test_config_flow.py @@ -0,0 +1,90 @@ +"""Tests for the Agent DVR config flow.""" +from homeassistant import data_entry_flow +from homeassistant.components.agent_dvr import config_flow +from homeassistant.components.agent_dvr.const import SERVER_URL +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort flow if Agent device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_PORT: 8090}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_connection_error(hass: HomeAssistant, aioclient_mock) -> None: + """Test we show user form on Agent connection error.""" + + aioclient_mock.get("http://example.local:8090/command.cgi?cmd=getStatus", text="") + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_PORT: 8090}, + ) + + assert result["errors"] == {"base": "device_unavailable"} + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:8090/command.cgi?cmd=getStatus", + text=load_fixture("agent_dvr/status.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + "http://example.local:8090/command.cgi?cmd=getObjects", + text=load_fixture("agent_dvr/objects.json"), + headers={"Content-Type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090} + ) + + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_PORT] == 8090 + assert result["data"][SERVER_URL] == "http://example.local:8090/" + assert result["title"] == "DESKTOP" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entries = hass.config_entries.async_entries(config_flow.DOMAIN) + assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 83e7d5d210e..243a92258eb 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -2,7 +2,6 @@ import json from airly.exceptions import AirlyError -from asynctest import patch from homeassistant import data_entry_flow from homeassistant.components.airly.const import DOMAIN @@ -15,6 +14,7 @@ from homeassistant.const import ( HTTP_FORBIDDEN, ) +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = { diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index d21aec14fa0..fcb2360b6c6 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,44 +1,67 @@ """Define tests for the AirVisual config flow.""" -from asynctest import patch -from pyairvisual.errors import InvalidKeyError +from pyairvisual.errors import InvalidKeyError, NodeProError from homeassistant import data_entry_flow -from homeassistant.components.airvisual import CONF_GEOGRAPHIES, DOMAIN +from homeassistant.components.airvisual import ( + CONF_GEOGRAPHIES, + CONF_INTEGRATION_TYPE, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY, + INTEGRATION_TYPE_NODE_PRO, +) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, + CONF_IP_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE, + CONF_PASSWORD, CONF_SHOW_ON_MAP, ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry async def test_duplicate_error(hass): - """Test that errors are shown when duplicates are added.""" - conf = { + """Test that errors are shown when duplicate entries are added.""" + geography_conf = { CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, } + node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "12345"} MockConfigEntry( - domain=DOMAIN, unique_id="51.528308, -0.3817765", data=conf + domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + MockConfigEntry( + domain=DOMAIN, unique_id="192.168.1.100", data=node_pro_conf + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=node_pro_conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_invalid_api_key(hass): - """Test that invalid credentials throws an error.""" - conf = { +async def test_invalid_identifier(hass): + """Test that an invalid API key or Node/Pro ID throws an error.""" + geography_conf = { CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, @@ -48,13 +71,31 @@ async def test_invalid_api_key(hass): "pyairvisual.api.API.nearest_city", side_effect=InvalidKeyError, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} -async def test_migration_1_2(hass): - """Test migrating from version 1 to version 2.""" +async def test_node_pro_error(hass): + """Test that an invalid Node/Pro ID shows an error.""" + node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} + + with patch( + "pyairvisual.node.Node.from_samba", side_effect=NodeProError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=node_pro_conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_IP_ADDRESS: "unable_to_connect"} + + +async def test_migration(hass): + """Test migrating from version 1 to the current version.""" conf = { CONF_API_KEY: "abcde12345", CONF_GEOGRAPHIES: [ @@ -83,6 +124,7 @@ async def test_migration_1_2(hass): CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, } assert config_entries[1].unique_id == "35.48847, 137.5263065" @@ -91,17 +133,22 @@ async def test_migration_1_2(hass): CONF_API_KEY: "abcde12345", CONF_LATITUDE: 35.48847, CONF_LONGITUDE: 137.5263065, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, } async def test_options_flow(hass): """Test config flow options.""" - conf = {CONF_API_KEY: "abcde12345"} + geography_conf = { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + } config_entry = MockConfigEntry( domain=DOMAIN, - unique_id="abcde12345", - data=conf, + unique_id="51.528308, -0.3817765", + data=geography_conf, options={CONF_SHOW_ON_MAP: True}, ) config_entry.add_to_hass(hass) @@ -122,18 +169,8 @@ async def test_options_flow(hass): assert config_entry.options == {CONF_SHOW_ON_MAP: False} -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - -async def test_step_import(hass): - """Test that the import step works.""" +async def test_step_geography(hass): + """Test the geograph (cloud API) step.""" conf = { CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, @@ -146,6 +183,52 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Cloud API (51.528308, -0.3817765)" + assert result["data"] == { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, + } + + +async def test_step_node_pro(hass): + """Test the Node/Pro step.""" + conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} + + with patch( + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyairvisual.node.Node.from_samba"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Node/Pro (192.168.1.100)" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "my_password", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + } + + +async def test_step_import(hass): + """Test the import step for both types of configuration.""" + geography_conf = { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + } + + with patch( + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyairvisual.api.API.nearest_city"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Cloud API (51.528308, -0.3817765)" @@ -153,27 +236,33 @@ async def test_step_import(hass): CONF_API_KEY: "abcde12345", CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY, } async def test_step_user(hass): - """Test that the user step works.""" - conf = { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 32.87336, - CONF_LONGITUDE: -117.22743, - } + """Test the user ("pick the integration type") step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Cloud API (32.87336, -117.22743)" - assert result["data"] == { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 32.87336, - CONF_LONGITUDE: -117.22743, - } + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_GEOGRAPHY}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "geography" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={"type": INTEGRATION_TYPE_NODE_PRO}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "node_pro" diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 9b890aa4d25..9d414d179ab 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -69,6 +69,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "platform": "device", + "domain": DOMAIN, + "type": "arming", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, { "platform": "device", "domain": DOMAIN, diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py new file mode 100644 index 00000000000..257d764468a --- /dev/null +++ b/tests/components/alarm_control_panel/test_init.py @@ -0,0 +1,13 @@ +"""Tests for Alarm control panel.""" +from homeassistant.components import alarm_control_panel + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomAlarm(alarm_control_panel.AlarmControlPanel): + def supported_features(self): + pass + + CustomAlarm() + assert "AlarmControlPanel is deprecated, modify CustomAlarm" in caplog.text diff --git a/tests/components/alert/test_reproduce_state.py b/tests/components/alert/test_reproduce_state.py new file mode 100644 index 00000000000..2470106558c --- /dev/null +++ b/tests/components/alert/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Alert.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Alert states.""" + hass.states.async_set("alert.entity_off", "off", {}) + hass.states.async_set("alert.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "alert", "turn_on") + turn_off_calls = async_mock_service(hass, "alert", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("alert.entity_off", "off"), State("alert.entity_on", "on")] + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("alert.entity_off", "not_supported")] + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("alert.entity_on", "off"), + State("alert.entity_off", "on"), + # Should not raise + State("alert.non_existing", "on"), + ] + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "alert" + assert turn_on_calls[0].data == { + "entity_id": "alert.entity_off", + } + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "alert" + assert turn_off_calls[0].data == {"entity_id": "alert.entity_on"} diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1bef3a5ae12..591d200ef90 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,10 +1,10 @@ """Test for smart home alexa support.""" -from unittest.mock import patch import pytest from homeassistant.components.alexa import messages, smart_home import homeassistant.components.camera as camera +from homeassistant.components.cover import DEVICE_CLASS_GATE from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) import homeassistant.components.vacuum as vacuum +from homeassistant.config import async_process_ha_core_config from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter @@ -39,7 +40,8 @@ from . import ( reported_properties, ) -from tests.common import async_mock_service, mock_coro +from tests.async_mock import patch +from tests.common import async_mock_service @pytest.fixture @@ -2630,6 +2632,28 @@ async def test_cover_garage_door(hass): ) +async def test_cover_gate(hass): + """Test gate cover discovery.""" + device = ( + "cover.test_gate", + "off", + { + "friendly_name": "Test cover gate", + "supported_features": 3, + "device_class": DEVICE_CLASS_GATE, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_gate" + assert appliance["displayCategories"][0] == "GARAGE_DOOR" + assert appliance["friendlyName"] == "Test cover gate" + + assert_endpoint_capabilities( + appliance, "Alexa.ModeController", "Alexa.EndpointHealth", "Alexa" + ) + + async def test_cover_position_mode(hass): """Test cover discovery and position using modeController.""" device = ( @@ -3761,8 +3785,11 @@ async def test_camera_discovery(hass, mock_stream): "idle", {"friendly_name": "Test camera", "supported_features": 3}, ) - with patch( - "homeassistant.helpers.network.async_get_external_url", + + hass.config.components.add("cloud") + with patch.object( + hass.components.cloud, + "async_remote_ui_url", return_value="https://example.nabu.casa", ): appliance = await discovery_test(device, hass) @@ -3789,8 +3816,11 @@ async def test_camera_discovery_without_stream(hass): "idle", {"friendly_name": "Test camera", "supported_features": 3}, ) - with patch( - "homeassistant.helpers.network.async_get_external_url", + + hass.config.components.add("cloud") + with patch.object( + hass.components.cloud, + "async_remote_ui_url", return_value="https://example.nabu.casa", ): appliance = await discovery_test(device, hass) @@ -3803,8 +3833,7 @@ async def test_camera_discovery_without_stream(hass): [ ("http://nohttpswrongport.org:8123", 2), ("http://nohttpsport443.org:443", 2), - ("tls://nohttpsport443.org:443", 2), - ("https://httpsnnonstandport.org:8123", 3), + ("https://httpsnnonstandport.org:8123", 2), ("https://correctschemaandport.org:443", 3), ("https://correctschemaandport.org", 3), ], @@ -3816,11 +3845,12 @@ async def test_camera_hass_urls(hass, mock_stream, url, result): "idle", {"friendly_name": "Test camera", "supported_features": 3}, ) - with patch( - "homeassistant.helpers.network.async_get_external_url", return_value=url - ): - appliance = await discovery_test(device, hass) - assert len(appliance["capabilities"]) == result + await async_process_ha_core_config( + hass, {"external_url": url}, + ) + + appliance = await discovery_test(device, hass) + assert len(appliance["capabilities"]) == result async def test_initialize_camera_stream(hass, mock_camera, mock_stream): @@ -3829,12 +3859,13 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): "Alexa.CameraStreamController", "InitializeCameraStreams", "camera#demo_camera" ) + await async_process_ha_core_config( + hass, {"external_url": "https://mycamerastream.test"}, + ) + with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=mock_coro("rtsp://example.local"), - ), patch( - "homeassistant.helpers.network.async_get_external_url", - return_value="https://mycamerastream.test", + return_value="rtsp://example.local", ): msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request) await hass.async_block_till_done() diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 0b4869ee2a6..d1403c017aa 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,14 +1,13 @@ """Test the Almond config flow.""" import asyncio -from asynctest import patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow from homeassistant.components.almond.const import DOMAIN from homeassistant.helpers import config_entry_oauth2_flow -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry CLIENT_ID_VALUE = "1234" CLIENT_SECRET_VALUE = "5678" @@ -16,7 +15,7 @@ CLIENT_SECRET_VALUE = "5678" async def test_import(hass): """Test that we can import a config entry.""" - with patch("pyalmond.WebAlmondAPI.async_list_apps", side_effect=mock_coro): + with patch("pyalmond.WebAlmondAPI.async_list_apps"): assert await setup.async_setup_component( hass, "almond", diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index d5b8deefd5e..9fb228dbf66 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -1,16 +1,17 @@ """Tests for Almond set up.""" from time import time -from unittest.mock import patch import pytest from homeassistant import config_entries, core from homeassistant.components.almond import const +from homeassistant.config import async_process_ha_core_config from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture(autouse=True) @@ -34,18 +35,16 @@ async def test_set_up_oauth_remote_url(hass, aioclient_mock): with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - return_value=mock_coro(), ): assert await async_setup_component(hass, "almond", {}) assert entry.state == config_entries.ENTRY_STATE_LOADED + hass.config.components.add("cloud") with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( - "homeassistant.helpers.network.async_get_external_url", + "homeassistant.helpers.network.get_url", return_value="https://example.nabu.casa", - ), patch( - "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() - ) as mock_create_device: + ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow()) @@ -69,7 +68,6 @@ async def test_set_up_oauth_no_external_url(hass, aioclient_mock): with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - return_value=mock_coro(), ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) @@ -97,16 +95,20 @@ async def test_set_up_hassio(hass, aioclient_mock): async def test_set_up_local(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we use Hass.io.""" + """Test we do not set up Almond to connect to HA if we use local.""" + + # Set up an internal URL, as Almond won't be set up if there is no URL available + await async_process_ha_core_config( + hass, {"internal_url": "https://192.168.0.1"}, + ) + entry = MockConfigEntry( domain="almond", data={"type": const.TYPE_LOCAL, "host": "http://localhost:9999"}, ) entry.add_to_hass(hass) - with patch( - "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() - ) as mock_create_device: + with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) assert entry.state == config_entries.ENTRY_STATE_LOADED diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index acf3717b898..6dee15c27f9 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the Ambiclimate config flow.""" -from unittest.mock import Mock, patch - import ambiclimate from homeassistant import data_entry_flow @@ -8,7 +6,7 @@ from homeassistant.components.ambiclimate import config_flow from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch async def init_config_flow(hass): @@ -68,9 +66,7 @@ async def test_full_flow_implementation(hass): assert "response_type=code" in url assert "redirect_uri=https%3A%2F%2Fhass.com%2Fapi%2Fambiclimate" in url - with patch( - "ambiclimate.AmbiclimateOAuth.get_access_token", return_value=mock_coro("test") - ): + with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Ambiclimate" @@ -78,9 +74,7 @@ async def test_full_flow_implementation(hass): assert result["data"]["client_secret"] == "secret" assert result["data"]["client_id"] == "id" - with patch( - "ambiclimate.AmbiclimateOAuth.get_access_token", return_value=mock_coro(None) - ): + with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -97,9 +91,7 @@ async def test_abort_invalid_code(hass): config_flow.register_flow_implementation(hass, None, None) flow = await init_config_flow(hass) - with patch( - "ambiclimate.AmbiclimateOAuth.get_access_token", return_value=mock_coro(None) - ): + with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "access_token" @@ -119,7 +111,7 @@ async def test_already_setup(hass): async def test_view(hass): """Test view.""" - hass.config_entries.flow.async_init = Mock() + hass.config_entries.flow.async_init = AsyncMock() request = aiohttp.MockRequest(b"", query_string="code=test_code") request.app = {"hass": hass} diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index c49b6ad11e9..85c80dd0b1c 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,6 +1,6 @@ """Define patches used for androidtv tests.""" -from unittest.mock import mock_open, patch +from tests.async_mock import mock_open, patch class AdbDeviceTcpFake: diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index c9f2c271000..fa4f6ffbed6 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,7 +1,6 @@ """The tests for the androidtv platform.""" import base64 import logging -from unittest.mock import patch from androidtv.exceptions import LockNotAcquiredException @@ -41,6 +40,8 @@ from homeassistant.setup import async_setup_component from . import patchers +from tests.async_mock import patch + # Android TV device with Python ADB implementation CONFIG_ANDROIDTV_PYTHON_ADB = { DOMAIN: { diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index e73b65661c9..1c93158ec03 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,7 +1,6 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access import json -from unittest.mock import patch from aiohttp import web import pytest @@ -12,6 +11,7 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 61092899e24..5c69e19435e 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -1,7 +1,6 @@ """The tests for the APNS component.""" import io import unittest -from unittest.mock import Mock, mock_open, patch from apns2.errors import Unregistered import yaml @@ -11,6 +10,7 @@ import homeassistant.components.notify as notify from homeassistant.core import State from homeassistant.setup import setup_component +from tests.async_mock import Mock, mock_open, patch from tests.common import assert_setup_component, get_test_home_assistant CONFIG = { diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 8135f4e8e2c..125971016cb 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,8 +1,8 @@ """The tests for the apprise notification platform.""" -from unittest.mock import MagicMock, patch - from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + BASE_COMPONENT = "notify" diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index dc0cf09f28d..95cdf4befec 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,11 +1,10 @@ """Test APRS device tracker.""" -from unittest.mock import Mock, patch - import aprslib import homeassistant.components.aprs.device_tracker as device_tracker from homeassistant.const import EVENT_HOMEASSISTANT_START +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant DEFAULT_PORT = 14580 diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index ec9c6bb1f36..e515b71468b 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -1,7 +1,6 @@ """Tests for the arcam_fmj component.""" from arcam.fmj.client import Client from arcam.fmj.state import State -from asynctest import Mock import pytest from homeassistant.components.arcam_fmj import DEVICE_SCHEMA @@ -9,6 +8,8 @@ from homeassistant.components.arcam_fmj.const import DOMAIN from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT +from tests.async_mock import Mock + MOCK_HOST = "127.0.0.1" MOCK_PORT = 1234 MOCK_TURN_ON = { diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index a6b36a71d1c..5a73e770129 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -2,7 +2,6 @@ from math import isclose from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes -from asynctest.mock import ANY, MagicMock, Mock, PropertyMock, patch import pytest from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC @@ -10,6 +9,8 @@ from homeassistant.core import HomeAssistant from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT +from tests.async_mock import ANY, MagicMock, Mock, PropertyMock, patch + MOCK_TURN_ON = { "service": "switch.turn_on", "data": {"entity_id": "switch.test"}, diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 15b85959ff0..e75db4a57dd 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the Netgear Arlo sensors.""" from collections import namedtuple -from unittest.mock import MagicMock, patch import pytest @@ -12,6 +11,8 @@ from homeassistant.const import ( UNIT_PERCENTAGE, ) +from tests.async_mock import patch + def _get_named_tuple(input_dict): return namedtuple("Struct", input_dict.keys())(*input_dict.values()) @@ -94,7 +95,7 @@ def sensor_with_hass_data(default_sensor, hass): def mock_dispatch(): """Mock the dispatcher connect method.""" target = "homeassistant.components.arlo.sensor.async_dispatcher_connect" - with patch(target, MagicMock()) as _mock: + with patch(target) as _mock: yield _mock diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index 3954808aa37..ed733b54d25 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,5 +1,4 @@ """The tests for the ASUSWRT device tracker platform.""" -from unittest.mock import patch from homeassistant.components.asuswrt import ( CONF_DNSMASQ, @@ -10,13 +9,13 @@ from homeassistant.components.asuswrt import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock, patch async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_connect = AsyncMock() AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} @@ -27,7 +26,7 @@ async def test_password_or_pub_key_required(hass): async def test_network_unreachable(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func(exception=OSError) + AsusWrt().connection.async_connect = AsyncMock(side_effect=OSError) AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} @@ -39,10 +38,8 @@ async def test_network_unreachable(hass): async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() - AsusWrt().connection.async_get_connected_devices = mock_coro_func( - return_value={} - ) + AsusWrt().connection.async_connect = AsyncMock() + AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={}) result = await async_setup_component( hass, DOMAIN, @@ -62,7 +59,7 @@ async def test_get_scanner_with_password_no_pubkey(hass): async def test_specify_non_directory_path_for_dnsmasq(hass): """Test creating an AsusWRT scanner with a dnsmasq location which is not a valid directory.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_connect = AsyncMock() AsusWrt().is_connected = False result = await async_setup_component( hass, @@ -82,10 +79,8 @@ async def test_specify_non_directory_path_for_dnsmasq(hass): async def test_interface(hass): """Test creating an AsusWRT scanner using interface eth1.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() - AsusWrt().connection.async_get_connected_devices = mock_coro_func( - return_value={} - ) + AsusWrt().connection.async_connect = AsyncMock() + AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={}) result = await async_setup_component( hass, DOMAIN, @@ -106,7 +101,7 @@ async def test_interface(hass): async def test_no_interface(hass): """Test creating an AsusWRT scanner using no interface.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_connect = AsyncMock() AsusWrt().is_connected = False result = await async_setup_component( hass, diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 991be0bac50..6d58b909280 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from aioasuswrt.asuswrt import Device -from asynctest import CoroutineMock, patch from homeassistant.components import sensor from homeassistant.components.asuswrt import ( @@ -20,6 +19,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed VALID_CONFIG_ROUTER_SSH = { @@ -54,10 +54,10 @@ MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] async def test_sensors(hass: HomeAssistant): """Test creating an AsusWRT sensor.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = CoroutineMock() - AsusWrt().async_get_connected_devices = CoroutineMock(return_value=MOCK_DEVICES) - AsusWrt().async_get_bytes_total = CoroutineMock(return_value=MOCK_BYTES_TOTAL) - AsusWrt().async_get_current_transfer_rates = CoroutineMock( + AsusWrt().connection.async_connect = AsyncMock() + AsusWrt().async_get_connected_devices = AsyncMock(return_value=MOCK_DEVICES) + AsusWrt().async_get_bytes_total = AsyncMock(return_value=MOCK_BYTES_TOTAL) + AsusWrt().async_get_current_transfer_rates = AsyncMock( return_value=MOCK_CURRENT_TRANSFER_RATES ) diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index bda4ccc9023..65583340524 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,17 +1,16 @@ """Tests for the Atag config flow.""" -from unittest.mock import PropertyMock - -from asynctest import patch from pyatag import AtagException from homeassistant import config_entries, data_entry_flow from homeassistant.components.atag import DOMAIN -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT +from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { CONF_HOST: "127.0.0.1", + CONF_EMAIL: "test@domain.com", CONF_PORT: 10000, } FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() @@ -44,7 +43,7 @@ async def test_connection_error(hass): """Test we show user form on Atag connection error.""" with patch( - "homeassistant.components.atag.config_flow.AtagDataStore.async_check_pair_status", + "homeassistant.components.atag.config_flow.AtagOne.authorize", side_effect=AtagException(), ): result = await hass.config_entries.flow.async_init( @@ -60,10 +59,10 @@ async def test_connection_error(hass): async def test_full_flow_implementation(hass): """Test registering an integration and finishing flow works.""" - with patch( - "homeassistant.components.atag.AtagDataStore.async_check_pair_status", + with patch("homeassistant.components.atag.AtagOne.authorize",), patch( + "homeassistant.components.atag.AtagOne.update", ), patch( - "homeassistant.components.atag.AtagDataStore.device", + "homeassistant.components.atag.AtagOne.id", new_callable=PropertyMock(return_value="device_identifier"), ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 62249e0fb1e..c471dfca2a9 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -3,8 +3,6 @@ import json import os import time -from asynctest import mock -from asynctest.mock import CoroutineMock, MagicMock, PropertyMock from august.activity import ( ACTIVITY_ACTIONS_DOOR_OPERATION, ACTIVITY_ACTIONS_DOORBELL_DING, @@ -29,6 +27,8 @@ from homeassistant.components.august import ( ) from homeassistant.setup import async_setup_component +# from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import load_fixture @@ -43,10 +43,8 @@ def _mock_get_config(): } -@mock.patch("homeassistant.components.august.gateway.ApiAsync") -@mock.patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate" -) +@patch("homeassistant.components.august.gateway.ApiAsync") +@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock): """Set up august integration.""" authenticate_mock.side_effect = MagicMock( @@ -109,15 +107,19 @@ async def _create_august_with_devices( def lock_return_activities_side_effect(access_token, device_id): lock = _get_device_detail("locks", device_id) return [ - _mock_lock_operation_activity(lock, "lock"), - _mock_door_operation_activity(lock, "doorclosed"), + # There is a check to prevent out of order events + # so we set the doorclosed & lock event in the future + # to prevent a race condition where we reject the event + # because it happened before the dooropen & unlock event. + _mock_lock_operation_activity(lock, "lock", 2000), + _mock_door_operation_activity(lock, "doorclosed", 2000), ] def unlock_return_activities_side_effect(access_token, device_id): lock = _get_device_detail("locks", device_id) return [ - _mock_lock_operation_activity(lock, "unlock"), - _mock_door_operation_activity(lock, "dooropen"), + _mock_lock_operation_activity(lock, "unlock", 0), + _mock_door_operation_activity(lock, "dooropen", 0), ] if "get_lock_detail" not in api_call_side_effects: @@ -146,37 +148,37 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects): api_instance = MagicMock(name="Api") if api_call_side_effects["get_lock_detail"]: - type(api_instance).async_get_lock_detail = CoroutineMock( + type(api_instance).async_get_lock_detail = AsyncMock( side_effect=api_call_side_effects["get_lock_detail"] ) if api_call_side_effects["get_operable_locks"]: - type(api_instance).async_get_operable_locks = CoroutineMock( + type(api_instance).async_get_operable_locks = AsyncMock( side_effect=api_call_side_effects["get_operable_locks"] ) if api_call_side_effects["get_doorbells"]: - type(api_instance).async_get_doorbells = CoroutineMock( + type(api_instance).async_get_doorbells = AsyncMock( side_effect=api_call_side_effects["get_doorbells"] ) if api_call_side_effects["get_doorbell_detail"]: - type(api_instance).async_get_doorbell_detail = CoroutineMock( + type(api_instance).async_get_doorbell_detail = AsyncMock( side_effect=api_call_side_effects["get_doorbell_detail"] ) if api_call_side_effects["get_house_activities"]: - type(api_instance).async_get_house_activities = CoroutineMock( + type(api_instance).async_get_house_activities = AsyncMock( side_effect=api_call_side_effects["get_house_activities"] ) if api_call_side_effects["lock_return_activities"]: - type(api_instance).async_lock_return_activities = CoroutineMock( + type(api_instance).async_lock_return_activities = AsyncMock( side_effect=api_call_side_effects["lock_return_activities"] ) if api_call_side_effects["unlock_return_activities"]: - type(api_instance).async_unlock_return_activities = CoroutineMock( + type(api_instance).async_unlock_return_activities = AsyncMock( side_effect=api_call_side_effects["unlock_return_activities"] ) @@ -288,10 +290,10 @@ async def _mock_doorsense_missing_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.online_missing_doorsense.json") -def _mock_lock_operation_activity(lock, action): +def _mock_lock_operation_activity(lock, action, offset): return LockOperationActivity( { - "dateTime": time.time() * 1000, + "dateTime": (time.time() + offset) * 1000, "deviceID": lock.device_id, "deviceType": "lock", "action": action, @@ -299,10 +301,10 @@ def _mock_lock_operation_activity(lock, action): ) -def _mock_door_operation_activity(lock, action): +def _mock_door_operation_activity(lock, action, offset): return DoorOperationActivity( { - "dateTime": time.time() * 1000, + "dateTime": (time.time() + offset) * 1000, "deviceID": lock.device_id, "deviceType": "lock", "action": action, diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index e47bafece42..3ec1b2d608c 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -1,9 +1,8 @@ """The camera tests for the august platform.""" -from asynctest import mock - from homeassistant.const import STATE_IDLE +from tests.async_mock import patch from tests.components.august.mocks import ( _create_august_with_devices, _mock_doorbell_from_fixture, @@ -14,7 +13,7 @@ async def test_create_doorbell(hass, aiohttp_client): """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") - with mock.patch.object( + with patch.object( doorbell_one, "async_get_doorbell_image", create=False, return_value="image" ): await _create_august_with_devices(hass, [doorbell_one]) diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 8d29ba650fa..ed75bc3685c 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,5 +1,4 @@ """Test the August config flow.""" -from asynctest import patch from august.authenticator import ValidationResult from homeassistant import config_entries, setup @@ -17,6 +16,8 @@ from homeassistant.components.august.exceptions import ( ) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from tests.async_mock import patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index f5fe35b4b19..ec035b9ec38 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,11 +1,8 @@ """The gateway tests for the august platform.""" -from unittest.mock import MagicMock - -from asynctest import mock - from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway +from tests.async_mock import MagicMock, patch from tests.components.august.mocks import _mock_august_authentication, _mock_get_config @@ -14,11 +11,9 @@ async def test_refresh_access_token(hass): await _patched_refresh_access_token(hass, "new_token", 5678) -@mock.patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate" -) -@mock.patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") -@mock.patch( +@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") +@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") +@patch( "homeassistant.components.august.gateway.AuthenticatorAsync.async_refresh_access_token" ) async def _patched_refresh_access_token( diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index c287a26b34f..f29403c9f21 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,7 +1,6 @@ """The tests for the august platform.""" import asyncio -from asynctest import patch from august.exceptions import AugustApiAIOHTTPError from homeassistant import setup @@ -27,6 +26,7 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.august.mocks import ( _create_august_with_devices, diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 4bd5509a216..dfa0edfcb6d 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -71,6 +71,7 @@ async def test_one_lock_operation(hass): assert await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True ) + await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_UNLOCKED @@ -84,6 +85,7 @@ async def test_one_lock_operation(hass): assert await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True ) + await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_LOCKED diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index b359144ab97..3d1ba068c85 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -1,12 +1,11 @@ """Tests for the client validator.""" import asyncio -from unittest.mock import patch import pytest from homeassistant.components.auth import indieauth -from tests.common import mock_coro +from tests.async_mock import patch from tests.test_util.aiohttp import AiohttpClientMocker @@ -113,9 +112,7 @@ async def test_verify_redirect_uri(): None, "http://ex.com", "http://ex.com/callback" ) - with patch.object( - indieauth, "fetch_redirect_uris", side_effect=lambda *_: mock_coro([]) - ): + with patch.object(indieauth, "fetch_redirect_uris", return_value=[]): # Different domain assert not await indieauth.verify_redirect_uri( None, "http://ex.com", "http://different.com/callback" @@ -174,9 +171,7 @@ async def test_find_link_tag_max_size(hass, mock_session): ) async def test_verify_redirect_uri_android_ios(client_id): """Test that we verify redirect uri correctly for Android/iOS.""" - with patch.object( - indieauth, "fetch_redirect_uris", side_effect=lambda *_: mock_coro([]) - ): + with patch.object(indieauth, "fetch_redirect_uris", return_value=[]): assert await indieauth.verify_redirect_uri( None, client_id, "homeassistant://auth-callback" ) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 2c9a39c6fb6..3d799fe0078 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,6 +1,5 @@ """Integration tests for the auth component.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.auth.models import Credentials from homeassistant.components import auth @@ -10,6 +9,7 @@ from homeassistant.util.dt import utcnow from . import async_setup_auth +from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index e6e5281d601..f2629a27bb9 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,8 +1,7 @@ """Tests for the login flow.""" -from unittest.mock import patch - from . import async_setup_auth +from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI diff --git a/tests/components/automatic/__init__.py b/tests/components/automatic/__init__.py deleted file mode 100644 index 4f7f83b97b5..00000000000 --- a/tests/components/automatic/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the automatic component.""" diff --git a/tests/components/automatic/test_device_tracker.py b/tests/components/automatic/test_device_tracker.py deleted file mode 100644 index 09ea7c61858..00000000000 --- a/tests/components/automatic/test_device_tracker.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Test the automatic device tracker platform.""" -from datetime import datetime -import logging - -import aioautomatic -from asynctest import MagicMock, patch - -from homeassistant.components.automatic.device_tracker import async_setup_scanner -from homeassistant.setup import async_setup_component - -_LOGGER = logging.getLogger(__name__) - - -@patch("aioautomatic.Client.create_session_from_refresh_token") -@patch("json.load") -@patch("json.dump") -@patch("os.makedirs") -@patch("os.path.isfile", return_value=True) -@patch("homeassistant.components.automatic.device_tracker.open", create=True) -def test_invalid_credentials( - mock_open, - mock_isfile, - mock_makedirs, - mock_json_dump, - mock_json_load, - mock_create_session, - hass, -): - """Test with invalid credentials.""" - hass.loop.run_until_complete(async_setup_component(hass, "http", {})) - mock_json_load.return_value = {"refresh_token": "bad_token"} - - async def get_session(*args, **kwargs): - """Return the test session.""" - raise aioautomatic.exceptions.BadRequestError("err_invalid_refresh_token") - - mock_create_session.side_effect = get_session - - config = { - "platform": "automatic", - "client_id": "client_id", - "secret": "client_secret", - "devices": None, - } - hass.loop.run_until_complete(async_setup_scanner(hass, config, None)) - assert mock_create_session.called - assert len(mock_create_session.mock_calls) == 1 - assert mock_create_session.mock_calls[0][1][0] == "bad_token" - - -@patch("aioautomatic.Client.create_session_from_refresh_token") -@patch("aioautomatic.Client.ws_connect") -@patch("json.load") -@patch("json.dump") -@patch("os.makedirs") -@patch("os.path.isfile", return_value=True) -@patch("homeassistant.components.automatic.device_tracker.open", create=True) -def test_valid_credentials( - mock_open, - mock_isfile, - mock_makedirs, - mock_json_dump, - mock_json_load, - mock_ws_connect, - mock_create_session, - hass, -): - """Test with valid credentials.""" - hass.loop.run_until_complete(async_setup_component(hass, "http", {})) - mock_json_load.return_value = {"refresh_token": "good_token"} - - session = MagicMock() - vehicle = MagicMock() - trip = MagicMock() - mock_see = MagicMock() - - vehicle.id = "mock_id" - vehicle.display_name = "mock_display_name" - vehicle.fuel_level_percent = 45.6 - vehicle.latest_location = None - vehicle.updated_at = datetime(2017, 8, 13, 1, 2, 3) - - trip.end_location.lat = 45.567 - trip.end_location.lon = 34.345 - trip.end_location.accuracy_m = 5.6 - trip.ended_at = datetime(2017, 8, 13, 1, 2, 4) - - async def get_session(*args, **kwargs): - """Return the test session.""" - return session - - async def get_vehicles(*args, **kwargs): - """Return list of test vehicles.""" - return [vehicle] - - async def get_trips(*args, **kwargs): - """Return list of test trips.""" - return [trip] - - mock_create_session.side_effect = get_session - session.ws_connect = MagicMock() - session.get_vehicles.side_effect = get_vehicles - session.get_trips.side_effect = get_trips - session.refresh_token = "mock_refresh_token" - - config = { - "platform": "automatic", - "username": "good_username", - "password": "good_password", - "client_id": "client_id", - "secret": "client_secret", - "devices": None, - } - result = hass.loop.run_until_complete(async_setup_scanner(hass, config, mock_see)) - - assert result - - assert mock_ws_connect.called - assert len(mock_ws_connect.mock_calls) == 2 - - assert mock_create_session.called - assert len(mock_create_session.mock_calls) == 1 - assert mock_create_session.mock_calls[0][1][0] == "good_token" - - assert mock_see.called - assert len(mock_see.mock_calls) == 2 - assert mock_see.mock_calls[0][2]["dev_id"] == "mock_id" - assert mock_see.mock_calls[0][2]["mac"] == "mock_id" - assert mock_see.mock_calls[0][2]["host_name"] == "mock_display_name" - assert mock_see.mock_calls[0][2]["attributes"] == {"fuel_level": 45.6} - assert mock_see.mock_calls[0][2]["gps"] == (45.567, 34.345) - assert mock_see.mock_calls[0][2]["gps_accuracy"] == 5.6 - - assert mock_json_dump.called - assert len(mock_json_dump.mock_calls) == 1 - assert mock_json_dump.mock_calls[0][1][0] == {"refresh_token": "mock_refresh_token"} diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 340bb6c1e95..cc9aecfdac4 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -46,7 +46,7 @@ async def test_if_fires_on_event(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_extra_data(hass, calls): @@ -64,14 +64,14 @@ async def test_if_fires_on_event_extra_data(hass, calls): hass.bus.async_fire("test_event", {"extra_key": "extra_data"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_with_data(hass, calls): @@ -93,7 +93,7 @@ async def test_if_fires_on_event_with_data(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_with_empty_data_config(hass, calls): @@ -119,7 +119,7 @@ async def test_if_fires_on_event_with_empty_data_config(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_with_nested_data(hass, calls): @@ -143,7 +143,7 @@ async def test_if_fires_on_event_with_nested_data(hass, calls): "test_event", {"parent_attr": {"some_attr": "some_value", "another": "value"}} ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_if_event_data_not_matches(hass, calls): @@ -165,4 +165,4 @@ async def test_if_not_fires_if_event_data_not_matches(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_other_value"}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py index 5daca51d0a1..99ace50e77d 100644 --- a/tests/components/automation/test_geo_location.py +++ b/tests/components/automation/test_geo_location.py @@ -83,11 +83,11 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert ( - "geo_location - geo_location.entity - hello - hello - test" - == calls[0].data["some"] + calls[0].data["some"] + == "geo_location - geo_location.entity - hello - hello - test" ) # Set out of zone again so we can trigger call @@ -108,7 +108,7 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): @@ -143,7 +143,7 @@ async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_zone_leave(hass, calls): @@ -178,7 +178,7 @@ async def test_if_fires_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): @@ -213,7 +213,7 @@ async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_zone_appear(hass, calls): @@ -258,10 +258,10 @@ async def test_if_fires_on_zone_appear(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert ( - "geo_location - geo_location.entity - - hello - test" == calls[0].data["some"] + calls[0].data["some"] == "geo_location - geo_location.entity - - hello - test" ) @@ -308,7 +308,7 @@ async def test_if_fires_on_zone_disappear(hass, calls): hass.states.async_remove("geo_location.entity") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ( - "geo_location - geo_location.entity - hello - - test" == calls[0].data["some"] + calls[0].data["some"] == "geo_location - geo_location.entity - hello - - test" ) diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index a0985e54976..d7bdfbeef3e 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -1,11 +1,10 @@ """The tests for the Event automation.""" -from unittest.mock import Mock, patch - import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, mock_coro +from tests.async_mock import AsyncMock, patch +from tests.common import async_mock_service async def test_if_fires_on_hass_start(hass): @@ -30,8 +29,7 @@ async def test_if_fires_on_hass_start(hass): assert len(calls) == 1 with patch( - "homeassistant.config.async_hass_config_yaml", - Mock(return_value=mock_coro(config)), + "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value=config), ): await hass.services.async_call( automation.DOMAIN, automation.SERVICE_RELOAD, blocking=True diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index cd4a01e9a28..7a082ba1931 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,6 +1,5 @@ """The tests for the automation component.""" from datetime import timedelta -from unittest.mock import Mock, patch import pytest @@ -19,6 +18,7 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -148,7 +148,7 @@ async def test_service_specify_entity_id(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID) @@ -170,7 +170,7 @@ async def test_service_specify_entity_id_list(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ["hello.world", "hello.world2"] == calls[0].data.get(ATTR_ENTITY_ID) @@ -192,10 +192,10 @@ async def test_two_triggers(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_trigger_service_ignoring_condition(hass, calls): @@ -268,17 +268,17 @@ async def test_two_conditions_with_and(hass, calls): hass.states.async_set(entity_id, 100) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 101) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 151) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_automation_list_setting(hass, calls): @@ -302,11 +302,11 @@ async def test_automation_list_setting(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.bus.async_fire("test_event_2") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_automation_calling_two_actions(hass, calls): @@ -368,7 +368,7 @@ async def test_shared_context(hass, calls): assert event_mock.call_count == 2 # Verify automation triggered evenet for 'hello' automation - args, kwargs = event_mock.call_args_list[0] + args, _ = event_mock.call_args_list[0] first_trigger_context = args[0].context assert first_trigger_context.parent_id == context.id # Ensure event data has all attributes set @@ -376,7 +376,7 @@ async def test_shared_context(hass, calls): assert args[0].data.get(ATTR_ENTITY_ID) is not None # Ensure context set correctly for event fired by 'hello' automation - args, kwargs = first_automation_listener.call_args + args, _ = first_automation_listener.call_args assert args[0].context is first_trigger_context # Ensure the 'hello' automation state has the right context @@ -385,7 +385,7 @@ async def test_shared_context(hass, calls): assert state.context is first_trigger_context # Verify automation triggered evenet for 'bye' automation - args, kwargs = event_mock.call_args_list[1] + args, _ = event_mock.call_args_list[1] second_trigger_context = args[0].context assert second_trigger_context.parent_id == first_trigger_context.id # Ensure event data has all attributes set diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index b8c369f5e63..0a07c5aac48 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -57,7 +57,7 @@ async def test_if_fires_on_topic_match(hass, calls): await hass.async_block_till_done() async_fire_mqtt_message(hass, "test-topic", "test_payload") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_topic_and_payload_match(hass, calls): @@ -79,7 +79,7 @@ async def test_if_fires_on_topic_and_payload_match(hass, calls): async_fire_mqtt_message(hass, "test-topic", "hello") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): @@ -101,7 +101,7 @@ async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): async_fire_mqtt_message(hass, "test-topic", "no-hello") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_encoding_default(hass, calls): diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index f779f022e65..46b774c1cc9 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,6 +1,5 @@ """The tests for numeric state automation.""" from datetime import timedelta -from unittest.mock import patch import pytest import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -52,7 +52,7 @@ async def test_if_fires_on_entity_change_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id # Set above 12 so the automation will fire again @@ -61,7 +61,7 @@ async def test_if_fires_on_entity_change_below(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_over_to_below(hass, calls): @@ -87,7 +87,7 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entities_change_over_to_below(hass, calls): @@ -114,10 +114,10 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity_1", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): @@ -144,18 +144,18 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): # 9 is below 10 so this should fire hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id # already below so should not fire again hass.states.async_set("test.entity", 5) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # still below so should not fire again hass.states.async_set("test.entity", 3) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): @@ -181,7 +181,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): # 10 is not below 10 so this should not fire again hass.states.async_set("test.entity", 10) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_initial_entity_below(hass, calls): @@ -207,7 +207,7 @@ async def test_if_fires_on_initial_entity_below(hass, calls): # Fire on first update even if initial state was already below hass.states.async_set("test.entity", 8) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_initial_entity_above(hass, calls): @@ -233,7 +233,7 @@ async def test_if_fires_on_initial_entity_above(hass, calls): # Fire on first update even if initial state was already above hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_above(hass, calls): @@ -255,7 +255,7 @@ async def test_if_fires_on_entity_change_above(hass, calls): # 11 is above 10 hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_below_to_above(hass, calls): @@ -282,7 +282,7 @@ async def test_if_fires_on_entity_change_below_to_above(hass, calls): # 11 is above 10 and 9 is below hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): @@ -309,12 +309,12 @@ async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): # 12 is above 10 so this should fire hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # already above, should not fire again hass.states.async_set("test.entity", 15) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): @@ -341,7 +341,7 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): # 10 is not above 10 so this should not fire again hass.states.async_set("test.entity", 10) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_entity_change_below_range(hass, calls): @@ -364,7 +364,7 @@ async def test_if_fires_on_entity_change_below_range(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_below_above_range(hass, calls): @@ -387,7 +387,7 @@ async def test_if_fires_on_entity_change_below_above_range(hass, calls): # 4 is below 5 hass.states.async_set("test.entity", 4) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): @@ -414,7 +414,7 @@ async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): @@ -441,7 +441,7 @@ async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): # 4 is below 5 so it should not fire hass.states.async_set("test.entity", 4) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_if_entity_not_match(hass, calls): @@ -463,7 +463,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): @@ -485,7 +485,7 @@ async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9, {"test_attribute": 11}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, calls): @@ -507,7 +507,7 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call # 11 is not below 10 hass.states.async_set("test.entity", 11, {"test_attribute": 9}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): @@ -530,7 +530,7 @@ async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": 9}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, calls): @@ -553,7 +553,7 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, c # 11 is not below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): @@ -576,7 +576,7 @@ async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): # 11 is not below 10, entity state value should not be tested hass.states.async_set("test.entity", "9", {"test_attribute": 11}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, calls): @@ -599,7 +599,7 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call # 11 is not below 10, entity state value should not be tested hass.states.async_set("test.entity", "entity") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, calls): @@ -624,7 +624,7 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, "test.entity", "entity", {"test_attribute": 9, "not_test_attribute": 11} ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_template_list(hass, calls): @@ -647,7 +647,7 @@ async def test_template_list(hass, calls): # 3 is below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 3]}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_template_string(hass, calls): @@ -686,10 +686,10 @@ async def test_template_string(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", "test state 2", {"test_attribute": "0.9"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ( - "numeric_state - test.entity - 10.0 - None - test state 1 - " - "test state 2" == calls[0].data["some"] + calls[0].data["some"] + == "numeric_state - test.entity - 10.0 - None - test state 1 - test state 2" ) @@ -715,7 +715,7 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(hass, "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 9} ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_action(hass, calls): @@ -742,19 +742,19 @@ async def test_if_action(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 8) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 9) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_fails_setup_bad_for(hass, calls): @@ -826,7 +826,7 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): @@ -853,7 +853,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 hass.states.async_set("test.entity_1", 15) hass.states.async_set("test.entity_2", 15) @@ -866,7 +866,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): @@ -897,11 +897,11 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity", 9, attributes={"mock_attr": "attr_change"}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=4) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for(hass, calls): @@ -927,7 +927,7 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_wait_template_with_trigger(hass, calls): @@ -965,7 +965,7 @@ async def test_wait_template_with_trigger(hass, calls): hass.states.async_set("test.entity", "8") await hass.async_block_till_done() await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert "numeric_state - test.entity - 12" == calls[0].data["some"] @@ -1000,16 +1000,16 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_entities_change_overlap(hass, calls): @@ -1052,18 +1052,18 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_change_with_for_template_1(hass, calls): @@ -1087,10 +1087,10 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_2(hass, calls): @@ -1114,10 +1114,10 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_3(hass, calls): @@ -1141,10 +1141,10 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_invalid_for_template(hass, calls): @@ -1215,22 +1215,22 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1 - 0:00:05" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 mock_utcnow.return_value += timedelta(seconds=5) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2 - 0:00:10" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" def test_below_above(): diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 949851f5470..0d591c967be 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -1,6 +1,5 @@ """The test for state automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -9,6 +8,7 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -65,15 +65,15 @@ async def test_if_fires_on_entity_change(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id - assert "state - test.entity - hello - world - None" == calls[0].data["some"] + assert calls[0].data["some"] == "state - test.entity - hello - world - None" await common.async_turn_off(hass) await hass.async_block_till_done() hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_from_filter(hass, calls): @@ -96,7 +96,7 @@ async def test_if_fires_on_entity_change_with_from_filter(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_to_filter(hass, calls): @@ -119,7 +119,7 @@ async def test_if_fires_on_entity_change_with_to_filter(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_attribute_change_with_to_filter(hass, calls): @@ -143,7 +143,7 @@ async def test_if_fires_on_attribute_change_with_to_filter(hass, calls): hass.states.async_set("test.entity", "world", {"test_attribute": 11}) hass.states.async_set("test.entity", "world", {"test_attribute": 12}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_both_filters(hass, calls): @@ -167,7 +167,7 @@ async def test_if_fires_on_entity_change_with_both_filters(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_if_to_filter_not_match(hass, calls): @@ -191,7 +191,7 @@ async def test_if_not_fires_if_to_filter_not_match(hass, calls): hass.states.async_set("test.entity", "moon") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_if_from_filter_not_match(hass, calls): @@ -217,7 +217,7 @@ async def test_if_not_fires_if_from_filter_not_match(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_if_entity_not_match(hass, calls): @@ -236,7 +236,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_action(hass, calls): @@ -262,13 +262,13 @@ async def test_if_action(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, test_state + "something") hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fails_setup_if_to_boolean_value(hass, calls): @@ -377,7 +377,7 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): @@ -404,7 +404,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 hass.states.async_set("test.entity_1", "world_no") hass.states.async_set("test.entity_2", "world_no") @@ -417,7 +417,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): @@ -450,11 +450,11 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): "test.entity", "world", attributes={"mock_attr": "attr_change"} ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=4) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for_multiple_force_update(hass, calls): @@ -481,16 +481,16 @@ async def test_if_fires_on_entity_change_with_for_multiple_force_update(hass, ca mock_utcnow.return_value = utcnow hass.states.async_set("test.force_entity", "world", None, True) await hass.async_block_till_done() - for _ in range(0, 4): + for _ in range(4): mock_utcnow.return_value += timedelta(seconds=1) async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.force_entity", "world", None, True) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=4) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for(hass, calls): @@ -539,7 +539,7 @@ async def test_if_fires_on_entity_removal(hass, calls): assert hass.states.async_remove("test.entity", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id @@ -571,13 +571,13 @@ async def test_if_fires_on_for_condition(hass, calls): # not enough time has passed hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Time travel 10 secs into the future mock_utcnow.return_value = point2 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_for_condition_attribute_change(hass, calls): @@ -609,7 +609,7 @@ async def test_if_fires_on_for_condition_attribute_change(hass, calls): # not enough time has passed hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Still not enough time has passed, but an attribute is changed mock_utcnow.return_value = point2 @@ -618,13 +618,13 @@ async def test_if_fires_on_for_condition_attribute_change(hass, calls): ) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Enough time has now passed mock_utcnow.return_value = point3 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fails_setup_for_without_time(hass, calls): @@ -707,8 +707,8 @@ async def test_wait_template_with_trigger(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 1 == len(calls) - assert "state - test.entity - hello - world" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "state - test.entity - hello - world" async def test_if_fires_on_entities_change_no_overlap(hass, calls): @@ -741,16 +741,16 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_entities_change_overlap(hass, calls): @@ -792,18 +792,18 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_change_with_for_template_1(hass, calls): @@ -826,10 +826,10 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_2(hass, calls): @@ -852,10 +852,10 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_3(hass, calls): @@ -878,10 +878,10 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_invalid_for_template_1(hass, calls): @@ -950,19 +950,19 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1 - 0:00:05" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 mock_utcnow.return_value += timedelta(seconds=5) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2 - 0:00:10" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 3468c9e9480..4efb19ff201 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -1,6 +1,5 @@ """The tests for the sun automation.""" from datetime import datetime -from unittest.mock import patch import pytest @@ -10,6 +9,7 @@ from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common @@ -59,7 +59,7 @@ async def test_sunset_trigger(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 with patch("homeassistant.util.dt.utcnow", return_value=now): await common.async_turn_on(hass) @@ -67,7 +67,7 @@ async def test_sunset_trigger(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_sunrise_trigger(hass, calls): @@ -89,7 +89,7 @@ async def test_sunrise_trigger(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_sunset_trigger_with_offset(hass, calls): @@ -121,8 +121,8 @@ async def test_sunset_trigger_with_offset(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) - assert "sun - sunset - 0:30:00" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "sun - sunset - 0:30:00" async def test_sunrise_trigger_with_offset(hass, calls): @@ -148,7 +148,7 @@ async def test_sunrise_trigger_with_offset(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_before_sunrise_no_offset(hass, calls): @@ -176,28 +176,28 @@ async def test_if_action_before_sunrise_no_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'before sunrise' true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' not true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_after_sunrise_no_offset(hass, calls): @@ -225,28 +225,28 @@ async def test_if_action_after_sunrise_no_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise + 1s -> 'after sunrise' true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'after sunrise' not true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight - 1s -> 'after sunrise' true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_sunrise_with_offset(hass, calls): @@ -278,56 +278,56 @@ async def test_if_action_before_sunrise_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise + 1h -> 'before sunrise' with offset +1h true now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC midnight -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC midnight - 1s -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' with offset +1h true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = sunset -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 1, 56, 48, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = sunset -1s -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 1, 56, 45, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_sunset_with_offset(hass, calls): @@ -359,56 +359,56 @@ async def test_if_action_before_sunset_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true now = datetime(2015, 9, 17, 2, 55, 25, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset + 1h -> 'before sunset' with offset +1h true now = datetime(2015, 9, 17, 2, 55, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = UTC midnight -> 'before sunset' with offset +1h true now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 3 == len(calls) + assert len(calls) == 3 # now = UTC midnight - 1s -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 4 == len(calls) + assert len(calls) == 4 # now = sunrise -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 5 == len(calls) + assert len(calls) == 5 # now = sunrise -1s -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 # now = local midnight-1s -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 async def test_if_action_after_sunrise_with_offset(hass, calls): @@ -440,70 +440,70 @@ async def test_if_action_after_sunrise_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise + 1h -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC noon -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 16, 12, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC noon - 1s -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 16, 11, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local noon -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 16, 19, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local noon - 1s -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 16, 18, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 3 == len(calls) + assert len(calls) == 3 # now = sunset -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 4 == len(calls) + assert len(calls) == 4 # now = sunset + 1s -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 5 == len(calls) + assert len(calls) == 5 # now = local midnight-1s -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 # now = local midnight -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 17, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 async def test_if_action_after_sunset_with_offset(hass, calls): @@ -535,28 +535,28 @@ async def test_if_action_after_sunset_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunset + 1h -> 'after sunset' with offset +1h true now = datetime(2015, 9, 16, 2, 56, 46, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = midnight-1s -> 'after sunset' with offset +1h true now = datetime(2015, 9, 16, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = midnight -> 'after sunset' with offset +1h not true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_and_after_during(hass, calls): @@ -588,35 +588,35 @@ async def test_if_action_before_and_after_during(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'after sunrise' + 'before sunset' true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset -> 'after sunrise' + 'before sunset' true now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = 9AM local -> 'after sunrise' + 'before sunset' true now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 3 == len(calls) + assert len(calls) == 3 async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): @@ -651,28 +651,28 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'before sunrise' true now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' true now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' not true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): @@ -707,28 +707,28 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunrise - 1s -> 'after sunrise' not true now = datetime(2015, 7, 24, 15, 17, 23, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'after sunrise' not true now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight - 1s -> 'after sunrise' true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): @@ -763,28 +763,28 @@ async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'before sunrise' true now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' true now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' not true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): @@ -819,25 +819,25 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset - 1s -> 'after sunset' not true now = datetime(2015, 7, 25, 11, 16, 26, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'after sunset' not true now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight - 1s -> 'after sunset' true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index 27e0d4f6965..91ecc4ad4ac 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -46,14 +46,14 @@ async def test_if_fires_on_change_bool(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_str(hass, calls): @@ -71,7 +71,7 @@ async def test_if_fires_on_change_str(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_str_crazy(hass, calls): @@ -89,7 +89,7 @@ async def test_if_fires_on_change_str_crazy(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_change_bool(hass, calls): @@ -107,7 +107,7 @@ async def test_if_not_fires_on_change_bool(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_change_str(hass, calls): @@ -125,7 +125,7 @@ async def test_if_not_fires_on_change_str(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_change_str_crazy(hass, calls): @@ -146,7 +146,7 @@ async def test_if_not_fires_on_change_str_crazy(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_no_change(hass, calls): @@ -186,12 +186,12 @@ async def test_if_fires_on_two_change(hass, calls): # Trigger once hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # Trigger again hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_template(hass, calls): @@ -212,7 +212,7 @@ async def test_if_fires_on_change_with_template(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_change_with_template(hass, calls): @@ -273,7 +273,7 @@ async def test_if_fires_on_change_with_template_advanced(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "template - test.entity - hello - world - None" == calls[0].data["some"] @@ -301,12 +301,12 @@ async def test_if_fires_on_no_change_with_template_advanced(hass, calls): # Different state hass.states.async_set("test.entity", "worldz") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Different state hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_change_with_template_2(hass, calls): @@ -374,17 +374,17 @@ async def test_if_action(hass, calls): # Condition is not true yet hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Change condition to true, but it shouldn't be triggered yet hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Condition is true and event is triggered hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_bad_template(hass, calls): @@ -420,7 +420,7 @@ async def test_if_fires_on_change_with_bad_template_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_wait_template_with_trigger(hass, calls): @@ -462,8 +462,8 @@ async def test_wait_template_with_trigger(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 1 == len(calls) - assert "template - test.entity - hello - world - None" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "template - test.entity - hello - world - None" async def test_if_fires_on_change_with_for(hass, calls): @@ -485,10 +485,10 @@ async def test_if_fires_on_change_with_for(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_advanced(hass, calls): @@ -527,10 +527,10 @@ async def test_if_fires_on_change_with_for_advanced(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "template - test.entity - hello - world - 0:00:05" == calls[0].data["some"] @@ -554,7 +554,7 @@ async def test_if_fires_on_change_with_for_0(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_0_advanced(hass, calls): @@ -593,9 +593,9 @@ async def test_if_fires_on_change_with_for_0_advanced(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id - assert "template - test.entity - hello - world - 0:00:00" == calls[0].data["some"] + assert calls[0].data["some"] == "template - test.entity - hello - world - 0:00:00" async def test_if_fires_on_change_with_for_2(hass, calls): @@ -617,10 +617,10 @@ async def test_if_fires_on_change_with_for_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_change_with_for(hass, calls): @@ -642,16 +642,16 @@ async def test_if_not_fires_on_change_with_for(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=6)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_when_turned_off_with_for(hass, calls): @@ -673,16 +673,16 @@ async def test_if_not_fires_when_turned_off_with_for(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 await common.async_turn_off(hass) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=6)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_change_with_for_template_1(hass, calls): @@ -704,10 +704,10 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_2(hass, calls): @@ -729,10 +729,10 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_3(hass, calls): @@ -754,10 +754,10 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_invalid_for_template_1(hass, calls): diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 511f8a305e6..0ba85467fcd 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -1,6 +1,5 @@ """The tests for the time automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -8,6 +7,7 @@ import homeassistant.components.automation as automation from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -49,8 +49,8 @@ async def test_if_fires_using_at(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=5, minute=0, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) - assert "time - 5" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "time - 5" async def test_if_not_fires_using_wrong_at(hass, calls): @@ -77,7 +77,7 @@ async def test_if_not_fires_using_wrong_at(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=0, second=5)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_action_before(hass, calls): @@ -101,13 +101,13 @@ async def test_if_action_before(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=after_10): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_after(hass, calls): @@ -131,13 +131,13 @@ async def test_if_action_after(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 with patch("homeassistant.helpers.condition.dt_util.now", return_value=after_10): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_one_weekday(hass, calls): @@ -162,13 +162,13 @@ async def test_if_action_one_weekday(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=tuesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_list_weekday(hass, calls): @@ -194,16 +194,16 @@ async def test_if_action_list_weekday(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=tuesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 with patch("homeassistant.helpers.condition.dt_util.now", return_value=wednesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py index 2c0574c3238..01aa32f318f 100644 --- a/tests/components/automation/test_time_pattern.py +++ b/tests/components/automation/test_time_pattern.py @@ -41,14 +41,14 @@ async def test_if_fires_when_hour_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_when_minute_matches(hass, calls): @@ -72,7 +72,7 @@ async def test_if_fires_when_minute_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(minute=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_when_second_matches(hass, calls): @@ -96,7 +96,7 @@ async def test_if_fires_when_second_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_when_all_matches(hass, calls): @@ -120,7 +120,7 @@ async def test_if_fires_when_all_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=3)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_periodic_seconds(hass, calls): @@ -144,7 +144,7 @@ async def test_if_fires_periodic_seconds(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0, minute=0, second=2)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_periodic_minutes(hass, calls): @@ -168,7 +168,7 @@ async def test_if_fires_periodic_minutes(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0, minute=2, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_periodic_hours(hass, calls): @@ -192,7 +192,7 @@ async def test_if_fires_periodic_hours(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=2, minute=0, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_default_values(hass, calls): @@ -211,14 +211,14 @@ async def test_default_values(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=1)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async_fire_time_changed(hass, dt_util.utcnow().replace(hour=2, minute=2, second=0)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index cb031486b6f..e80f70b10fe 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -81,7 +81,7 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "zone - test.entity - hello - hello - test" == calls[0].data["some"] @@ -99,7 +99,7 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): @@ -130,7 +130,7 @@ async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_zone_leave(hass, calls): @@ -161,7 +161,7 @@ async def test_if_fires_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): @@ -192,7 +192,7 @@ async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_zone_condition(hass, calls): @@ -220,4 +220,4 @@ async def test_zone_condition(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 2d88a5019b1..d1a3b933d05 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -4,7 +4,6 @@ from contextlib import contextmanager from datetime import timedelta import json import logging -from unittest.mock import patch from homeassistant.components.awair.sensor import ( ATTR_LAST_API_UPDATE, @@ -28,7 +27,8 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import parse_datetime, utcnow -from tests.common import async_fire_time_changed, load_fixture, mock_coro +from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture DISCOVERY_CONFIG = {"sensor": {"platform": "awair", "access_token": "qwerty"}} @@ -68,9 +68,9 @@ def alter_time(retval): async def setup_awair(hass, config=None, data_fixture=AIR_DATA_FIXTURE): """Load the Awair platform.""" devices_json = json.loads(load_fixture("awair_devices.json")) - devices_mock = mock_coro(devices_json) + devices_mock = devices_json devices_patch = patch("python_awair.AwairClient.devices", return_value=devices_mock) - air_data_mock = mock_coro(data_fixture) + air_data_mock = data_fixture air_data_patch = patch( "python_awair.AwairClient.air_data_latest", return_value=air_data_mock ) @@ -233,8 +233,7 @@ async def test_availability(hass): future = NOW + timedelta(minutes=30) data_patch = patch( - "python_awair.AwairClient.air_data_latest", - return_value=mock_coro(AIR_DATA_FIXTURE), + "python_awair.AwairClient.air_data_latest", return_value=AIR_DATA_FIXTURE, ) with data_patch, alter_time(future): @@ -246,9 +245,7 @@ async def test_availability(hass): future = NOW + timedelta(hours=1) fixture = AIR_DATA_FIXTURE_UPDATED fixture[0][ATTR_TIMESTAMP] = str(future) - data_patch = patch( - "python_awair.AwairClient.air_data_latest", return_value=mock_coro(fixture) - ) + data_patch = patch("python_awair.AwairClient.air_data_latest", return_value=fixture) with data_patch, alter_time(future): async_fire_time_changed(hass, future) @@ -258,9 +255,7 @@ async def test_availability(hass): future = NOW + timedelta(minutes=90) fixture = AIR_DATA_FIXTURE_EMPTY - data_patch = patch( - "python_awair.AwairClient.air_data_latest", return_value=mock_coro(fixture) - ) + data_patch = patch("python_awair.AwairClient.air_data_latest", return_value=fixture) with data_patch, alter_time(future): async_fire_time_changed(hass, future) @@ -276,7 +271,7 @@ async def test_async_update(hass): future = NOW + timedelta(minutes=10) data_patch = patch( "python_awair.AwairClient.air_data_latest", - return_value=mock_coro(AIR_DATA_FIXTURE_UPDATED), + return_value=AIR_DATA_FIXTURE_UPDATED, ) with data_patch, alter_time(future): @@ -300,7 +295,7 @@ async def test_throttle_async_update(hass): future = NOW + timedelta(minutes=1) data_patch = patch( "python_awair.AwairClient.air_data_latest", - return_value=mock_coro(AIR_DATA_FIXTURE_UPDATED), + return_value=AIR_DATA_FIXTURE_UPDATED, ) with data_patch, alter_time(future): diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index c7fa9d0a5c1..045ad2ff609 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,32 +1,32 @@ """Tests for the aws component config and setup.""" -from asynctest import CoroutineMock, MagicMock, patch as async_patch - from homeassistant.components import aws from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, MagicMock, patch as async_patch + class MockAioSession: """Mock AioSession.""" def __init__(self, *args, **kwargs): """Init a mock session.""" - self.get_user = CoroutineMock() - self.invoke = CoroutineMock() - self.publish = CoroutineMock() - self.send_message = CoroutineMock() + self.get_user = AsyncMock() + self.invoke = AsyncMock() + self.publish = AsyncMock() + self.send_message = AsyncMock() def create_client(self, *args, **kwargs): # pylint: disable=no-self-use """Create a mocked client.""" return MagicMock( - __aenter__=CoroutineMock( - return_value=CoroutineMock( + __aenter__=AsyncMock( + return_value=AsyncMock( get_user=self.get_user, # iam invoke=self.invoke, # lambda publish=self.publish, # sns send_message=self.send_message, # sqs ) ), - __aexit__=CoroutineMock(), + __aexit__=AsyncMock(), ) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 2e4c3e9f8be..d3972332c89 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,12 +1,11 @@ """Test Axis config flow.""" -from unittest.mock import Mock, patch - from homeassistant.components import axis from homeassistant.components.axis import config_flow from .test_device import MAC, MODEL, NAME, setup_axis_integration -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import Mock, patch +from tests.common import MockConfigEntry def setup_mock_axis_device(mock_device): @@ -80,7 +79,7 @@ async def test_manual_configuration_update_configuration(hass): with patch( "homeassistant.components.axis.config_flow.get_device", - return_value=mock_coro(mock_device), + return_value=mock_device, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -113,7 +112,7 @@ async def test_flow_fails_already_configured(hass): with patch( "homeassistant.components.axis.config_flow.get_device", - return_value=mock_coro(mock_device), + return_value=mock_device, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -232,7 +231,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): async def test_zeroconf_flow(hass): """Test that zeroconf discovery for new devices work.""" - with patch.object(axis, "get_device", return_value=mock_coro(Mock())): + with patch.object(axis, "get_device", return_value=Mock()): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 3d2ed432c1c..74b0ab3b992 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,13 +1,13 @@ """Test Axis device.""" from copy import deepcopy -from asynctest import Mock, patch import axis as axislib import pytest from homeassistant import config_entries from homeassistant.components import axis +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry MAC = "00408C12345" diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index d11f8c91fc3..b8baf18a67d 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,12 +1,11 @@ """Test Axis component setup process.""" -from unittest.mock import Mock, patch - from homeassistant.components import axis from homeassistant.setup import async_setup_component from .test_device import MAC, setup_axis_integration -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import MockConfigEntry async def test_setup_no_config(hass): @@ -30,7 +29,7 @@ async def test_setup_entry_fails(hass): config_entry.add_to_hass(hass) mock_device = Mock() - mock_device.async_setup.return_value = mock_coro(False) + mock_device.async_setup = AsyncMock(return_value=False) with patch.object(axis, "AxisNetworkDevice") as mock_device_class: mock_device_class.return_value = mock_device diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 844cfedf7fe..d8d69265f3a 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,13 +1,13 @@ """Axis switch platform tests.""" -from unittest.mock import Mock, call as mock_call - from homeassistant.components import axis import homeassistant.components.switch as switch from homeassistant.setup import async_setup_component from .test_device import NAME, setup_axis_integration +from tests.async_mock import Mock, call as mock_call + EVENTS = [ { "operation": "Initialized", diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 1ac24e03702..968b54b7892 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,6 +1,5 @@ """The test for binary_sensor device automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -12,6 +11,7 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 2299ba4c9a2..a40df84b899 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -1,24 +1,29 @@ """The tests for the Binary sensor component.""" -import unittest from unittest import mock from homeassistant.components import binary_sensor from homeassistant.const import STATE_OFF, STATE_ON -class TestBinarySensor(unittest.TestCase): - """Test the binary_sensor base class.""" +def test_state(): + """Test binary sensor state.""" + sensor = binary_sensor.BinarySensorEntity() + assert STATE_OFF == sensor.state + with mock.patch( + "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", new=False, + ): + assert STATE_OFF == binary_sensor.BinarySensorEntity().state + with mock.patch( + "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", new=True, + ): + assert STATE_ON == binary_sensor.BinarySensorEntity().state - def test_state(self): - """Test binary sensor state.""" - sensor = binary_sensor.BinarySensorDevice() - assert STATE_OFF == sensor.state - with mock.patch( - "homeassistant.components.binary_sensor.BinarySensorDevice.is_on", - new=False, - ): - assert STATE_OFF == binary_sensor.BinarySensorDevice().state - with mock.patch( - "homeassistant.components.binary_sensor.BinarySensorDevice.is_on", new=True, - ): - assert STATE_ON == binary_sensor.BinarySensorDevice().state + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomBinarySensor(binary_sensor.BinarySensorDevice): + pass + + CustomBinarySensor() + assert "BinarySensorDevice is deprecated, modify CustomBinarySensor" in caplog.text diff --git a/tests/components/blebox/__init__.py b/tests/components/blebox/__init__.py new file mode 100644 index 00000000000..00afae7ad28 --- /dev/null +++ b/tests/components/blebox/__init__.py @@ -0,0 +1 @@ +"""Tests for the blebox component.""" diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py new file mode 100644 index 00000000000..69798d01fb8 --- /dev/null +++ b/tests/components/blebox/conftest.py @@ -0,0 +1,87 @@ +"""PyTest fixtures and test helpers.""" + +from unittest import mock + +import blebox_uniapi +import pytest + +from homeassistant.components.blebox.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.async_mock import AsyncMock, PropertyMock, patch +from tests.common import MockConfigEntry + + +def patch_product_identify(path=None, **kwargs): + """Patch the blebox_uniapi Products class.""" + if path is None: + path = "homeassistant.components.blebox.Products" + patcher = patch(path, mock.DEFAULT, blebox_uniapi.products.Products, True, True) + products_class = patcher.start() + products_class.async_from_host = AsyncMock(**kwargs) + return products_class + + +def setup_product_mock(category, feature_mocks, path=None): + """Mock a product returning the given features.""" + + product_mock = mock.create_autospec( + blebox_uniapi.box.Box, True, True, features=None + ) + type(product_mock).features = PropertyMock(return_value={category: feature_mocks}) + + for feature in feature_mocks: + type(feature).product = PropertyMock(return_value=product_mock) + + patch_product_identify(path, return_value=product_mock) + return product_mock + + +def mock_only_feature(spec, **kwargs): + """Mock just the feature, without the product setup.""" + return mock.create_autospec(spec, True, True, **kwargs) + + +def mock_feature(category, spec, **kwargs): + """Mock a feature along with whole product setup.""" + feature_mock = mock_only_feature(spec, **kwargs) + feature_mock.async_update = AsyncMock() + product = setup_product_mock(category, [feature_mock]) + + type(feature_mock.product).name = PropertyMock(return_value="Some name") + type(feature_mock.product).type = PropertyMock(return_value="some type") + type(feature_mock.product).model = PropertyMock(return_value="some model") + type(feature_mock.product).brand = PropertyMock(return_value="BleBox") + type(feature_mock.product).firmware_version = PropertyMock(return_value="1.23") + type(feature_mock.product).unique_id = PropertyMock(return_value="abcd0123ef5678") + type(feature_mock).product = PropertyMock(return_value=product) + return feature_mock + + +def mock_config(ip_address="172.100.123.4"): + """Return a Mock of the HA entity config.""" + return MockConfigEntry(domain=DOMAIN, data={CONF_HOST: ip_address, CONF_PORT: 80}) + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: {CONF_HOST: "172.100.123.4", CONF_PORT: 80}} + + +@pytest.fixture(name="feature") +def feature(request): + """Return an entity wrapper from given fixture name.""" + return request.getfixturevalue(request.param) + + +async def async_setup_entity(hass, config, entity_id): + """Return a configured entity with the given entity_id.""" + config_entry = mock_config() + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + return entity_registry.async_get(entity_id) diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py new file mode 100644 index 00000000000..fe13dfae15e --- /dev/null +++ b/tests/components/blebox/test_config_flow.py @@ -0,0 +1,192 @@ +"""Test Home Assistant config flow for BleBox devices.""" + +import blebox_uniapi +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.blebox import config_flow +from homeassistant.setup import async_setup_component + +from .conftest import mock_config, mock_only_feature, setup_product_mock + +from tests.async_mock import DEFAULT, AsyncMock, PropertyMock, patch + + +def create_valid_feature_mock(path="homeassistant.components.blebox.Products"): + """Return a valid, complete BleBox feature mock.""" + feature = mock_only_feature( + blebox_uniapi.cover.Cover, + unique_id="BleBox-gateBox-1afe34db9437-0.position", + full_name="gateBox-0.position", + device_class="gate", + state=0, + async_update=AsyncMock(), + current=None, + ) + + product = setup_product_mock("covers", [feature], path) + + type(product).name = PropertyMock(return_value="My gate controller") + type(product).model = PropertyMock(return_value="gateController") + type(product).type = PropertyMock(return_value="gateBox") + type(product).brand = PropertyMock(return_value="BleBox") + type(product).firmware_version = PropertyMock(return_value="1.23") + type(product).unique_id = PropertyMock(return_value="abcd0123ef5678") + + return feature + + +@pytest.fixture +def valid_feature_mock(): + """Return a valid, complete BleBox feature mock.""" + return create_valid_feature_mock() + + +@pytest.fixture +def flow_feature_mock(): + """Return a mocked user flow feature.""" + return create_valid_feature_mock( + "homeassistant.components.blebox.config_flow.Products" + ) + + +async def test_flow_works(hass, valid_feature_mock, flow_feature_mock): + """Test that config flow works.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "My gate controller" + assert result["data"] == { + config_flow.CONF_HOST: "172.2.3.4", + config_flow.CONF_PORT: 80, + } + + +@pytest.fixture +def product_class_mock(): + """Return a mocked feature.""" + path = "homeassistant.components.blebox.config_flow.Products" + patcher = patch(path, DEFAULT, blebox_uniapi.products.Products, True, True) + yield patcher + + +async def test_flow_with_connection_failure(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock( + side_effect=blebox_uniapi.error.ConnectionError + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_with_api_failure(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock( + side_effect=blebox_uniapi.error.Error + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_with_unknown_failure(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock(side_effect=RuntimeError) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_with_unsupported_version(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock( + side_effect=blebox_uniapi.error.UnsupportedBoxVersion + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "unsupported_version"} + + +async def test_async_setup(hass): + """Test async_setup (for coverage).""" + assert await async_setup_component(hass, "blebox", {"host": "172.2.3.4"}) + await hass.async_block_till_done() + + +async def test_already_configured(hass, valid_feature_mock): + """Test that same device cannot be added twice.""" + + config = mock_config("172.2.3.4") + config.add_to_hass(hass) + + await hass.config_entries.async_setup(config.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "address_already_configured" + + +async def test_async_setup_entry(hass, valid_feature_mock): + """Test async_setup_entry (for coverage).""" + + config = mock_config() + config.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config.entry_id) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries() == [config] + assert config.state == config_entries.ENTRY_STATE_LOADED + + +async def test_async_remove_entry(hass, valid_feature_mock): + """Test async_setup_entry (for coverage).""" + + config = mock_config() + config.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_remove(config.entry_id) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries() == [] + assert config.state == config_entries.ENTRY_STATE_NOT_LOADED diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py new file mode 100644 index 00000000000..e7c20b10b2c --- /dev/null +++ b/tests/components/blebox/test_cover.py @@ -0,0 +1,422 @@ +"""BleBox cover entities tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHUTTER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_UNKNOWN, +) + +from .conftest import async_setup_entity, mock_feature + +from tests.async_mock import AsyncMock, PropertyMock + +ALL_COVER_FIXTURES = ["gatecontroller", "shutterbox", "gatebox"] +FIXTURES_SUPPORTING_STOP = ["gatecontroller", "shutterbox"] + + +@pytest.fixture(name="shutterbox") +def shutterbox_fixture(): + """Return a shutterBox fixture.""" + feature = mock_feature( + "covers", + blebox_uniapi.cover.Cover, + unique_id="BleBox-shutterBox-2bee34e750b8-position", + full_name="shutterBox-position", + device_class="shutter", + current=None, + state=None, + has_stop=True, + is_slider=True, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My shutter") + type(product).model = PropertyMock(return_value="shutterBox") + return (feature, "cover.shutterbox_position") + + +@pytest.fixture(name="gatebox") +def gatebox_fixture(): + """Return a gateBox fixture.""" + feature = mock_feature( + "covers", + blebox_uniapi.cover.Cover, + unique_id="BleBox-gateBox-1afe34db9437-position", + device_class="gatebox", + full_name="gateBox-position", + current=None, + state=None, + has_stop=False, + is_slider=False, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My gatebox") + type(product).model = PropertyMock(return_value="gateBox") + return (feature, "cover.gatebox_position") + + +@pytest.fixture(name="gatecontroller") +def gate_fixture(): + """Return a gateController fixture.""" + feature = mock_feature( + "covers", + blebox_uniapi.cover.Cover, + unique_id="BleBox-gateController-2bee34e750b8-position", + full_name="gateController-position", + device_class="gate", + current=None, + state=None, + has_stop=True, + is_slider=True, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My gate controller") + type(product).model = PropertyMock(return_value="gateController") + return (feature, "cover.gatecontroller_position") + + +async def test_init_gatecontroller(gatecontroller, hass, config): + """Test gateController default state.""" + + _, entity_id = gatecontroller + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-gateController-2bee34e750b8-position" + + state = hass.states.get(entity_id) + assert state.name == "gateController-position" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_GATE + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_OPEN + assert supported_features & SUPPORT_CLOSE + assert supported_features & SUPPORT_STOP + + assert supported_features & SUPPORT_SET_POSITION + assert ATTR_CURRENT_POSITION not in state.attributes + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My gate controller" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "gateController" + assert device.sw_version == "1.23" + + +async def test_init_shutterbox(shutterbox, hass, config): + """Test gateBox default state.""" + + _, entity_id = shutterbox + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-shutterBox-2bee34e750b8-position" + + state = hass.states.get(entity_id) + assert state.name == "shutterBox-position" + assert entry.device_class == DEVICE_CLASS_SHUTTER + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_OPEN + assert supported_features & SUPPORT_CLOSE + assert supported_features & SUPPORT_STOP + + assert supported_features & SUPPORT_SET_POSITION + assert ATTR_CURRENT_POSITION not in state.attributes + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My shutter" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "shutterBox" + assert device.sw_version == "1.23" + + +async def test_init_gatebox(gatebox, hass, config): + """Test cover default state.""" + + _, entity_id = gatebox + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-gateBox-1afe34db9437-position" + + state = hass.states.get(entity_id) + assert state.name == "gateBox-position" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_DOOR + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_OPEN + assert supported_features & SUPPORT_CLOSE + + # Not available during init since requires fetching state to detect + assert not supported_features & SUPPORT_STOP + + assert not supported_features & SUPPORT_SET_POSITION + assert ATTR_CURRENT_POSITION not in state.attributes + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My gatebox" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "gateBox" + assert device.sw_version == "1.23" + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_open(feature, hass, config): + """Test cover opening.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.state = 3 # manually stopped + + def open_gate(): + feature_mock.state = 1 # opening + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_open = AsyncMock(side_effect=open_gate) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_CLOSED + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_OPENING + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_close(feature, hass, config): + """Test cover closing.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.state = 4 # open + + def close(): + feature_mock.state = 0 # closing + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_close = AsyncMock(side_effect=close) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_OPEN + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_CLOSING + + +def opening_to_stop_feature_mock(feature_mock): + """Return an mocked feature which can be updated and stopped.""" + + def initial_update(): + feature_mock.state = 1 # opening + + def stop(): + feature_mock.state = 2 # manually stopped + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_stop = AsyncMock(side_effect=stop) + + +@pytest.mark.parametrize("feature", FIXTURES_SUPPORTING_STOP, indirect=["feature"]) +async def test_stop(feature, hass, config): + """Test cover stopping.""" + + feature_mock, entity_id = feature + opening_to_stop_feature_mock(feature_mock) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_OPENING + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OPEN + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_update(feature, hass, config): + """Test cover updating.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.current = 29 # inverted + feature_mock.state = 2 # manually stopped + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_CURRENT_POSITION] == 71 # 100 - 29 + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "feature", ["gatecontroller", "shutterbox"], indirect=["feature"] +) +async def test_set_position(feature, hass, config): + """Test cover position setting.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.state = 3 # closed + + def set_position(position): + assert position == 99 # inverted + feature_mock.state = 1 # opening + # feature_mock.current = position + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_set_position = AsyncMock(side_effect=set_position) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_CLOSED + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", + SERVICE_SET_COVER_POSITION, + {"entity_id": entity_id, ATTR_POSITION: 1}, + blocking=True, + ) # almost closed + assert hass.states.get(entity_id).state == STATE_OPENING + + +async def test_unknown_position(shutterbox, hass, config): + """Test cover position setting.""" + + feature_mock, entity_id = shutterbox + + def initial_update(): + feature_mock.state = 4 # opening + feature_mock.current = -1 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN + assert ATTR_CURRENT_POSITION not in state.attributes + + +async def test_with_stop(gatebox, hass, config): + """Test stop capability is available.""" + + feature_mock, entity_id = gatebox + opening_to_stop_feature_mock(feature_mock) + feature_mock.has_stop = True + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_STOP + + +async def test_with_no_stop(gatebox, hass, config): + """Test stop capability is not available.""" + + feature_mock, entity_id = gatebox + opening_to_stop_feature_mock(feature_mock) + feature_mock.has_stop = False + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert not supported_features & SUPPORT_STOP + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_update_failure(feature, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = feature + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_opening_state(feature, hass, config): + """Test that entity properties work.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.state = 1 # opening + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_OPENING + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_closing_state(feature, hass, config): + """Test that entity properties work.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.state = 0 # closing + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_CLOSING + + +@pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) +async def test_closed_state(feature, hass, config): + """Test that entity properties work.""" + + feature_mock, entity_id = feature + + def initial_update(): + feature_mock.state = 3 # closed + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_CLOSED diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py new file mode 100644 index 00000000000..098c10f2cfc --- /dev/null +++ b/tests/components/blebox/test_init.py @@ -0,0 +1,60 @@ +"""BleBox devices setup tests.""" + +import logging + +import blebox_uniapi + +from homeassistant.components.blebox.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY + +from .conftest import mock_config, patch_product_identify + + +async def test_setup_failure(hass, caplog): + """Test that setup failure is handled and logged.""" + + patch_product_identify(None, side_effect=blebox_uniapi.error.ClientError) + + entry = mock_config() + entry.add_to_hass(hass) + + caplog.set_level(logging.ERROR) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "Identify failed at 172.100.123.4:80 ()" in caplog.text + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_setup_failure_on_connection(hass, caplog): + """Test that setup failure is handled and logged.""" + + patch_product_identify(None, side_effect=blebox_uniapi.error.ConnectionError) + + entry = mock_config() + entry.add_to_hass(hass) + + caplog.set_level(logging.ERROR) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "Identify failed at 172.100.123.4:80 ()" in caplog.text + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry(hass): + """Test that unloading works properly.""" + patch_product_identify(None) + + entry = mock_config() + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) + + assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py new file mode 100644 index 00000000000..a19e628181c --- /dev/null +++ b/tests/components/blebox/test_sensor.py @@ -0,0 +1,88 @@ +"""Blebox sensors tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_TEMPERATURE, + STATE_UNKNOWN, + TEMP_CELSIUS, +) + +from .conftest import async_setup_entity, mock_feature + +from tests.async_mock import AsyncMock, PropertyMock + + +@pytest.fixture(name="tempsensor") +def tempsensor_fixture(): + """Return a default sensor mock.""" + feature = mock_feature( + "sensors", + blebox_uniapi.sensor.Temperature, + unique_id="BleBox-tempSensor-1afe34db9437-0.temperature", + full_name="tempSensor-0.temperature", + device_class="temperature", + unit="celsius", + current=None, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My temperature sensor") + type(product).model = PropertyMock(return_value="tempSensor") + return (feature, "sensor.tempsensor_0_temperature") + + +async def test_init(tempsensor, hass, config): + """Test sensor default state.""" + + _, entity_id = tempsensor + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-tempSensor-1afe34db9437-0.temperature" + + state = hass.states.get(entity_id) + assert state.name == "tempSensor-0.temperature" + + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My temperature sensor" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "tempSensor" + assert device.sw_version == "1.23" + + +async def test_update(tempsensor, hass, config): + """Test sensor update.""" + + feature_mock, entity_id = tempsensor + + def initial_update(): + feature_mock.current = 25.18 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.state == "25.18" + + +async def test_update_failure(tempsensor, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = tempsensor + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 308371c9aaa..af4df463339 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,7 +1,6 @@ """Test Bluetooth LE device tracker.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.device_tracker.const import ( @@ -13,6 +12,7 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify +from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/bom/test_sensor.py b/tests/components/bom/test_sensor.py index 7a9daa26c2b..6e85dbca1cd 100644 --- a/tests/components/bom/test_sensor.py +++ b/tests/components/bom/test_sensor.py @@ -2,7 +2,6 @@ import json import re import unittest -from unittest.mock import patch from urllib.parse import urlparse import requests @@ -11,6 +10,7 @@ from homeassistant.components import sensor from homeassistant.components.bom.sensor import BOMCurrentData from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 93d1bccba73..87fa26c242a 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,11 +1,10 @@ """Define tests for the Bravia TV config flow.""" -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from tests.async_mock import patch from tests.common import MockConfigEntry BRAVIA_SYSTEM_INFO = { diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index 1bdad193f52..5a359896bfa 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -1,14 +1,16 @@ """The tests for the broadlink component.""" from base64 import b64decode from datetime import timedelta -from unittest.mock import MagicMock, call, patch import pytest from homeassistant.components.broadlink import async_setup_service, data_packet from homeassistant.components.broadlink.const import DOMAIN, SERVICE_LEARN, SERVICE_SEND +from homeassistant.components.broadlink.device import BroadlinkDevice from homeassistant.util.dt import utcnow +from tests.async_mock import MagicMock, call, patch + DUMMY_IR_PACKET = ( "JgBGAJKVETkRORA6ERQRFBEUERQRFBE5ETkQOhAVEBUQFREUEBUQ" "OhEUERQRORE5EBURFBA6EBUQOhE5EBUQFRA6EDoRFBEADQUAAA==" @@ -33,39 +35,37 @@ async def test_padding(hass): async def test_send(hass): """Test send service.""" - mock_device = MagicMock() - mock_device.send_data.return_value = None - - async_setup_service(hass, DUMMY_HOST, mock_device) - await hass.async_block_till_done() + mock_api = MagicMock() + mock_api.send_data.return_value = None + device = BroadlinkDevice(hass, mock_api) + await async_setup_service(hass, DUMMY_HOST, device) await hass.services.async_call( DOMAIN, SERVICE_SEND, {"host": DUMMY_HOST, "packet": (DUMMY_IR_PACKET)} ) await hass.async_block_till_done() - assert mock_device.send_data.call_count == 1 - assert mock_device.send_data.call_args == call(b64decode(DUMMY_IR_PACKET)) + assert device.api.send_data.call_count == 1 + assert device.api.send_data.call_args == call(b64decode(DUMMY_IR_PACKET)) async def test_learn(hass): """Test learn service.""" - mock_device = MagicMock() - mock_device.enter_learning.return_value = None - mock_device.check_data.return_value = b64decode(DUMMY_IR_PACKET) + mock_api = MagicMock() + mock_api.enter_learning.return_value = None + mock_api.check_data.return_value = b64decode(DUMMY_IR_PACKET) + device = BroadlinkDevice(hass, mock_api) with patch.object( hass.components.persistent_notification, "async_create" ) as mock_create: - async_setup_service(hass, DUMMY_HOST, mock_device) - await hass.async_block_till_done() - + await async_setup_service(hass, DUMMY_HOST, device) await hass.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST}) await hass.async_block_till_done() - assert mock_device.enter_learning.call_count == 1 - assert mock_device.enter_learning.call_args == call() + assert device.api.enter_learning.call_count == 1 + assert device.api.enter_learning.call_args == call() assert mock_create.call_count == 1 assert mock_create.call_args == call( @@ -75,12 +75,12 @@ async def test_learn(hass): async def test_learn_timeout(hass): """Test learn service.""" - mock_device = MagicMock() - mock_device.enter_learning.return_value = None - mock_device.check_data.return_value = None + mock_api = MagicMock() + mock_api.enter_learning.return_value = None + mock_api.check_data.return_value = None + device = BroadlinkDevice(hass, mock_api) - async_setup_service(hass, DUMMY_HOST, mock_device) - await hass.async_block_till_done() + await async_setup_service(hass, DUMMY_HOST, device) now = utcnow() @@ -93,8 +93,8 @@ async def test_learn_timeout(hass): await hass.services.async_call(DOMAIN, SERVICE_LEARN, {"host": DUMMY_HOST}) await hass.async_block_till_done() - assert mock_device.enter_learning.call_count == 1 - assert mock_device.enter_learning.call_args == call() + assert device.api.enter_learning.call_count == 1 + assert device.api.enter_learning.call_args == call() assert mock_create.call_count == 1 assert mock_create.call_args == call( diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index d6c1fedd31d..1a3ba2a3e20 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1,11 +1,10 @@ """Tests for Brother Printer integration.""" import json -from asynctest import patch - from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 3f07fca49f0..06e58b83522 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -1,7 +1,6 @@ """Define tests for the Brother Printer config flow.""" import json -from asynctest import patch from brother import SnmpError, UnsupportedModel from homeassistant import data_entry_flow @@ -9,6 +8,7 @@ from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 13378e9dbb9..04c3c130fc9 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,6 +1,4 @@ """Test init of Brother integration.""" -from asynctest import patch - from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -9,6 +7,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE +from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.brother import init_integration diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index e88c22f3f40..8e1d52fd2a8 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -2,8 +2,6 @@ from datetime import timedelta import json -from asynctest import patch - from homeassistant.components.brother.const import UNIT_PAGES from homeassistant.const import ( ATTR_ENTITY_ID, @@ -16,6 +14,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.brother import init_integration diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py new file mode 100644 index 00000000000..1541555de55 --- /dev/null +++ b/tests/components/bsblan/__init__.py @@ -0,0 +1,44 @@ +"""Tests for the bsblan integration.""" + +from homeassistant.components.bsblan.const import ( + CONF_DEVICE_IDENT, + CONF_PASSKEY, + DOMAIN, +) +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def init_integration( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the BSBLan integration in Home Assistant.""" + + aioclient_mock.post( + "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", + params={"Parameter": "6224,6225,6226"}, + text=load_fixture("bsblan/info.json"), + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="RVS21.831F/127", + data={ + CONF_HOST: "example.local", + CONF_PASSKEY: "1234", + CONF_PORT: 80, + CONF_DEVICE_IDENT: "RVS21.831F/127", + }, + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py new file mode 100644 index 00000000000..48f71a8404f --- /dev/null +++ b/tests/components/bsblan/test_config_flow.py @@ -0,0 +1,92 @@ +"""Tests for the BSBLan device config flow.""" +import aiohttp + +from homeassistant import data_entry_flow +from homeassistant.components.bsblan import config_flow +from homeassistant.components.bsblan.const import CONF_DEVICE_IDENT, CONF_PASSKEY +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on BSBLan connection error.""" + aioclient_mock.post( + "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", + exc=aiohttp.ClientError, + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80}, + ) + + assert result["errors"] == {"base": "connection_error"} + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if BSBLan device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", + text=load_fixture("bsblan/info.json"), + headers={"Content-Type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80}, + ) + + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_PASSKEY] == "1234" + assert result["data"][CONF_PORT] == 80 + assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" + assert result["title"] == "RVS21.831F/127" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entries = hass.config_entries.async_entries(config_flow.DOMAIN) + assert entries[0].unique_id == "RVS21.831F/127" diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index fa6f331363f..1b6c21c7358 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,8 +1,6 @@ """The tests for the webdav calendar component.""" import datetime -from unittest.mock import MagicMock, Mock -from asynctest import patch from caldav.objects import Event import pytest @@ -10,6 +8,8 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt +from tests.async_mock import MagicMock, Mock, patch + # pylint: disable=redefined-outer-name DEVICE_DATA = {"name": "Private Calendar", "device_id": "Private Calendar"} diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index e8ab05abb90..36ee9b8aabf 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -2,19 +2,19 @@ import asyncio import base64 import io -from unittest.mock import PropertyMock, mock_open -from asynctest import patch import pytest from homeassistant.components import camera from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import PropertyMock, mock_open, patch from tests.components.camera import common @@ -242,6 +242,9 @@ async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): async def test_handle_play_stream_service(hass, mock_camera, mock_stream): """Test camera play_stream service.""" + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) await async_setup_component(hass, "media_player", {}) with patch( "homeassistant.components.camera.request_stream" diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index 819d1ce0e90..a3f6fbd7e2d 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,10 +1,10 @@ """The tests for the Canary component.""" import unittest -from unittest.mock import MagicMock, PropertyMock, patch from homeassistant import setup import homeassistant.components.canary as canary +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import get_test_home_assistant diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 1d559dbb7ba..5a4a82ccc5a 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Canary sensor platform.""" import copy import unittest -from unittest.mock import Mock from homeassistant.components.canary import DATA_CANARY, sensor as canary from homeassistant.components.canary.sensor import ( @@ -14,6 +13,7 @@ from homeassistant.components.canary.sensor import ( ) from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from tests.async_mock import Mock from tests.common import get_test_home_assistant from tests.components.canary.test_init import mock_device, mock_location diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 2ec02da7669..8a3a935429f 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,14 +1,16 @@ """Test Home Assistant Cast.""" -from unittest.mock import Mock, patch - from homeassistant.components.cast import home_assistant_cast +from homeassistant.config import async_process_ha_core_config +from tests.async_mock import patch from tests.common import MockConfigEntry, async_mock_signal async def test_service_show_view(hass): """Test we don't set app id in prod.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) @@ -21,7 +23,7 @@ async def test_service_show_view(hass): assert len(calls) == 1 controller, entity_id, view_path, url_path = calls[0] - assert controller.hass_url == "http://example.com" + assert controller.hass_url == "https://example.com" assert controller.client_id is None # Verify user did not accidentally submit their dev app id assert controller.supporting_app_id == "B12CE3CA" @@ -32,7 +34,9 @@ async def test_service_show_view(hass): async def test_service_show_view_dashboard(hass): """Test casting a specific dashboard.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) @@ -56,13 +60,17 @@ async def test_service_show_view_dashboard(hass): async def test_use_cloud_url(hass): """Test that we fall back to cloud url.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) + hass.config.components.add("cloud") + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) with patch( "homeassistant.components.cloud.async_remote_ui_url", - return_value="https://something.nabu.acas", + return_value="https://something.nabu.casa", ): await hass.services.async_call( "cast", @@ -73,4 +81,4 @@ async def test_use_cloud_url(hass): assert len(calls) == 1 controller = calls[0][0] - assert controller.hass_url == "https://something.nabu.acas" + assert controller.hass_url == "https://something.nabu.casa" diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 6971c071353..8f194668e56 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,19 +1,18 @@ """Tests for the Cast config flow.""" -from unittest.mock import patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro +from tests.async_mock import patch async def test_creating_entry_sets_up_media_player(hass): """Test setting up Cast loads the media player.""" with patch( "homeassistant.components.cast.media_player.async_setup_entry", - return_value=mock_coro(True), - ) as mock_setup, MockDependency("pychromecast", "discovery"), patch( + return_value=True, + ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -34,8 +33,8 @@ async def test_creating_entry_sets_up_media_player(hass): async def test_configuring_cast_creates_entry(hass): """Test that specifying config will create an entry.""" with patch( - "homeassistant.components.cast.async_setup_entry", return_value=mock_coro(True) - ) as mock_setup, MockDependency("pychromecast", "discovery"), patch( + "homeassistant.components.cast.async_setup_entry", return_value=True + ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=True ): await async_setup_component( @@ -49,8 +48,8 @@ async def test_configuring_cast_creates_entry(hass): async def test_not_configuring_cast_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch( - "homeassistant.components.cast.async_setup_entry", return_value=mock_coro(True) - ) as mock_setup, MockDependency("pychromecast", "discovery"), patch( + "homeassistant.components.cast.async_setup_entry", return_value=True + ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=True ): await async_setup_component(hass, cast.DOMAIN, {}) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 41cc50f33ae..2adb8b63052 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1,7 +1,6 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access from typing import Optional -from unittest.mock import MagicMock, Mock, patch from uuid import UUID import attr @@ -14,7 +13,8 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import MagicMock, Mock, patch +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) @@ -468,7 +468,6 @@ async def test_entry_setup_no_config(hass: HomeAssistantType): with patch( "homeassistant.components.cast.media_player._async_setup_platform", - return_value=mock_coro(), ) as mock_setup: await cast.async_setup_entry(hass, MockConfigEntry(), None) @@ -484,7 +483,6 @@ async def test_entry_setup_single_config(hass: HomeAssistantType): with patch( "homeassistant.components.cast.media_player._async_setup_platform", - return_value=mock_coro(), ) as mock_setup: await cast.async_setup_entry(hass, MockConfigEntry(), None) @@ -500,7 +498,6 @@ async def test_entry_setup_list_config(hass: HomeAssistantType): with patch( "homeassistant.components.cast.media_player._async_setup_platform", - return_value=mock_coro(), ) as mock_setup: await cast.async_setup_entry(hass, MockConfigEntry(), None) @@ -517,7 +514,7 @@ async def test_entry_setup_platform_not_ready(hass: HomeAssistantType): with patch( "homeassistant.components.cast.media_player._async_setup_platform", - return_value=mock_coro(exception=Exception), + side_effect=Exception, ) as mock_setup: with pytest.raises(PlatformNotReady): await cast.async_setup_entry(hass, MockConfigEntry(), None) diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 1b2cc175dcb..9618525ef32 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -2,14 +2,13 @@ import socket import ssl -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from .const import HOST, PORT +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index d4419b48370..3a2aeb84734 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -1,8 +1,6 @@ """Tests for Cert Expiry setup.""" from datetime import timedelta -from asynctest import patch - from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED @@ -12,6 +10,7 @@ import homeassistant.util.dt as dt_util from .const import HOST, PORT +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 6594b0988e7..9fcd1ac3efe 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -3,13 +3,12 @@ from datetime import timedelta import socket import ssl -from asynctest import patch - from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util from .const import HOST, PORT +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 4345ecedcf7..e42bf8c7e3c 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,6 +1,5 @@ """The tests for the climate component.""" from typing import List -from unittest.mock import MagicMock import pytest import voluptuous as vol @@ -10,8 +9,10 @@ from homeassistant.components.climate import ( HVAC_MODE_OFF, SET_TEMPERATURE_SCHEMA, ClimateDevice, + ClimateEntity, ) +from tests.async_mock import MagicMock from tests.common import async_mock_service @@ -45,7 +46,7 @@ async def test_set_temp_schema(hass, caplog): assert calls[-1].data == data -class MockClimateDevice(ClimateDevice): +class MockClimateEntity(ClimateEntity): """Mock Climate device to use in tests.""" @property @@ -67,7 +68,7 @@ class MockClimateDevice(ClimateDevice): async def test_sync_turn_on(hass): """Test if async turn_on calls sync turn_on.""" - climate = MockClimateDevice() + climate = MockClimateEntity() climate.hass = hass climate.turn_on = MagicMock() @@ -78,10 +79,24 @@ async def test_sync_turn_on(hass): async def test_sync_turn_off(hass): """Test if async turn_off calls sync turn_off.""" - climate = MockClimateDevice() + climate = MockClimateEntity() climate.hass = hass climate.turn_off = MagicMock() await climate.async_turn_off() assert climate.turn_off.called + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomClimate(ClimateDevice): + def hvac_mode(self): + pass + + def hvac_modes(self): + pass + + CustomClimate() + assert "ClimateDevice is deprecated, modify CustomClimate" in caplog.text diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 571b73e8d09..da7c6ff13d0 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,18 +1,17 @@ """Tests for the cloud component.""" -from unittest.mock import patch from homeassistant.components import cloud from homeassistant.components.cloud import const from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch async def mock_cloud(hass, config=None): """Mock cloud.""" assert await async_setup_component(hass, cloud.DOMAIN, {"cloud": config or {}}) cloud_inst = hass.data["cloud"] - with patch("hass_nabucasa.Cloud.run_executor", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.run_executor", AsyncMock(return_value=None)): await cloud_inst.start() diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 4755d470418..02d9b4c41aa 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,6 +1,4 @@ """Fixtures for cloud tests.""" -from unittest.mock import patch - import jwt import pytest @@ -8,6 +6,8 @@ from homeassistant.components.cloud import const, prefs from . import mock_cloud, mock_cloud_prefs +from tests.async_mock import patch + @pytest.fixture(autouse=True) def mock_user_data(): diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index a8c247cc985..ce310001b35 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -2,7 +2,6 @@ import asyncio import logging from time import time -from unittest.mock import Mock, patch import pytest @@ -11,7 +10,8 @@ from homeassistant.components.cloud import account_link from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_coro, mock_platform +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" @@ -42,12 +42,10 @@ async def test_setup_provide_implementation(hass): with patch( "homeassistant.components.cloud.account_link._get_services", - side_effect=lambda _: mock_coro( - [ - {"service": "test", "min_version": "0.1.0"}, - {"service": "too_new", "min_version": "100.0.0"}, - ] - ), + return_value=[ + {"service": "test", "min_version": "0.1.0"}, + {"service": "too_new", "min_version": "100.0.0"}, + ], ): assert ( await config_entry_oauth2_flow.async_get_implementations( @@ -77,7 +75,7 @@ async def test_get_services_cached(hass): with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( "hass_nabucasa.account_link.async_fetch_available_services", - side_effect=lambda _: mock_coro(services), + side_effect=lambda _: services, ) as mock_fetch: assert await account_link._get_services(hass) == 1 @@ -98,6 +96,18 @@ async def test_get_services_cached(hass): assert await account_link._get_services(hass) == 4 +async def test_get_services_error(hass): + """Test that we cache services.""" + hass.data["cloud"] = None + + with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( + "hass_nabucasa.account_link.async_fetch_available_services", + side_effect=asyncio.TimeoutError, + ): + assert await account_link._get_services(hass) == [] + assert account_link.DATA_SERVICES not in hass.data + + async def test_implementation(hass, flow_handler): """Test Cloud OAuth2 implementation.""" hass.data["cloud"] = None @@ -111,7 +121,7 @@ async def test_implementation(hass, flow_handler): flow_finished = asyncio.Future() helper = Mock( - async_get_authorize_url=Mock(return_value=mock_coro("http://example.com/auth")), + async_get_authorize_url=AsyncMock(return_value="http://example.com/auth"), async_get_tokens=Mock(return_value=flow_finished), ) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index f65b810d690..b064a5c9605 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,12 +1,12 @@ """Test Alexa config.""" import contextlib -from unittest.mock import Mock, patch from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import async_fire_time_changed async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): @@ -28,7 +28,7 @@ async def test_alexa_config_report_state(hass, cloud_prefs): assert conf.should_report_state is False assert conf.is_reporting_states is False - with patch.object(conf, "async_get_access_token", return_value=mock_coro("hello")): + with patch.object(conf, "async_get_access_token", AsyncMock(return_value="hello")): await cloud_prefs.async_update(alexa_report_state=True) await hass.async_block_till_done() @@ -60,7 +60,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", - auth=Mock(async_check_token=Mock(side_effect=mock_coro)), + auth=Mock(async_check_token=AsyncMock()), websession=hass.helpers.aiohttp_client.async_get_clientsession(), ), ) @@ -89,7 +89,7 @@ def patch_sync_helper(): to_update = [] to_remove = [] - async def sync_helper(to_upd, to_rem): + def sync_helper(to_upd, to_rem): to_update.extend([ent_id for ent_id in to_upd if ent_id not in to_update]) to_remove.extend([ent_id for ent_id in to_rem if ent_id not in to_remove]) return True @@ -189,13 +189,9 @@ async def test_alexa_update_report_state(hass, cloud_prefs): alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) with patch( - "homeassistant.components.cloud.alexa_config.AlexaConfig." - "async_sync_entities", - side_effect=mock_coro, + "homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities", ) as mock_sync, patch( - "homeassistant.components.cloud.alexa_config." - "AlexaConfig.async_enable_proactive_mode", - side_effect=mock_coro, + "homeassistant.components.cloud.alexa_config.AlexaConfig.async_enable_proactive_mode", ): await cloud_prefs.async_update(alexa_report_state=True) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index c4ad22abb2f..6a2d76dc403 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,11 +1,9 @@ """Tests for the cloud binary sensor.""" -from unittest.mock import Mock - -from asynctest import patch - from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch + async def test_remote_connection_sensor(hass): """Test the remote connection sensor.""" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index b9e6524b62e..21eb59ddc03 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,6 +1,4 @@ """Test the cloud.iot module.""" -from unittest.mock import MagicMock, patch - from aiohttp import web import pytest @@ -12,7 +10,7 @@ from homeassistant.setup import async_setup_component from . import mock_cloud, mock_cloud_prefs -from tests.common import mock_coro +from tests.async_mock import AsyncMock, MagicMock, patch from tests.components.alexa import test_smart_home as test_alexa @@ -130,7 +128,7 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): """Test handler Google Actions when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): assert await async_setup_component(hass, "cloud", {}) reqid = "5711642932632160983" @@ -145,7 +143,7 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): async def test_webhook_msg(hass): """Test webhook msg.""" - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup cloud = hass.data["cloud"] @@ -221,7 +219,7 @@ async def test_set_username(hass): prefs = MagicMock( alexa_enabled=False, google_enabled=False, - async_set_username=MagicMock(return_value=mock_coro()), + async_set_username=AsyncMock(return_value=None), ) client = CloudClient(hass, prefs, None, {}, {}) client.cloud = MagicMock(is_logged_in=True, username="mock-username") diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index b08b950a590..3808f9b179c 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,8 +1,4 @@ """Test the Cloud Google Config.""" -from unittest.mock import Mock - -from asynctest import patch - from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers @@ -11,7 +7,8 @@ from homeassistant.core import CoreState from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import async_fire_time_changed async def test_google_update_report_state(hass, cloud_prefs): @@ -43,12 +40,12 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, - Mock(auth=Mock(async_check_token=Mock(side_effect=mock_coro))), + Mock(auth=Mock(async_check_token=AsyncMock())), ) with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=mock_coro(Mock(status=HTTP_NOT_FOUND)), + return_value=Mock(status=HTTP_NOT_FOUND), ) as mock_request_sync: assert await config.async_sync_entities("user") == HTTP_NOT_FOUND assert len(mock_request_sync.mock_calls) == 1 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 2cfca8e6b92..df506d2d8fc 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,9 +1,7 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from ipaddress import ip_network -from unittest.mock import MagicMock, Mock -from asynctest import patch from hass_nabucasa import thingtalk from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED @@ -20,7 +18,7 @@ from homeassistant.core import State from . import mock_cloud, mock_cloud_prefs -from tests.common import mock_coro +from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.components.google_assistant import MockConfig SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" @@ -29,9 +27,7 @@ SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" @pytest.fixture() def mock_auth(): """Mock check token.""" - with patch( - "hass_nabucasa.auth.CognitoAuth.async_check_token", side_effect=mock_coro - ): + with patch("hass_nabucasa.auth.CognitoAuth.async_check_token"): yield @@ -89,7 +85,7 @@ async def test_google_actions_sync(mock_cognito, mock_cloud_login, cloud_client) """Test syncing Google Actions.""" with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=mock_coro(Mock(status=200)), + return_value=Mock(status=200), ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 200 @@ -100,7 +96,7 @@ async def test_google_actions_sync_fails(mock_cognito, mock_cloud_login, cloud_c """Test syncing Google Actions gone bad.""" with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=mock_coro(Mock(status=HTTP_INTERNAL_SERVER_ERROR)), + return_value=Mock(status=HTTP_INTERNAL_SERVER_ERROR), ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == HTTP_INTERNAL_SERVER_ERROR @@ -109,7 +105,7 @@ async def test_google_actions_sync_fails(mock_cognito, mock_cloud_login, cloud_c async def test_login_view(hass, cloud_client): """Test logging in.""" - hass.data["cloud"] = MagicMock(login=MagicMock(return_value=mock_coro())) + hass.data["cloud"] = MagicMock(login=AsyncMock()) req = await cloud_client.post( "/api/cloud/login", json={"email": "my_username", "password": "my_password"} @@ -184,7 +180,7 @@ async def test_login_view_unknown_error(cloud_client): async def test_logout_view(hass, cloud_client): """Test logging out.""" cloud = hass.data["cloud"] = MagicMock() - cloud.logout.return_value = mock_coro() + cloud.logout = AsyncMock(return_value=None) req = await cloud_client.post("/api/cloud/logout") assert req.status == 200 data = await req.json() @@ -450,8 +446,7 @@ async def test_websocket_subscription_not_logged_in(hass, hass_ws_client): """Test querying the status.""" client = await hass_ws_client(hass) with patch( - "hass_nabucasa.Cloud.fetch_subscription_info", - return_value=mock_coro({"return": "value"}), + "hass_nabucasa.Cloud.fetch_subscription_info", return_value={"return": "value"}, ): await client.send_json({"id": 5, "type": "cloud/subscription"}) response = await client.receive_json() @@ -529,7 +524,7 @@ async def test_enabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_logi """Test we call right code to enable webhooks.""" client = await hass_ws_client(hass) with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value=mock_coro() + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value={} ) as mock_enable: await client.send_json( {"id": 5, "type": "cloud/cloudhook/create", "webhook_id": "mock-webhook-id"} @@ -544,9 +539,7 @@ async def test_enabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_logi async def test_disabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_login): """Test we call right code to disable webhooks.""" client = await hass_ws_client(hass) - with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_delete", return_value=mock_coro() - ) as mock_disable: + with patch("hass_nabucasa.cloudhooks.Cloudhooks.async_delete") as mock_disable: await client.send_json( {"id": 5, "type": "cloud/cloudhook/delete", "webhook_id": "mock-webhook-id"} ) @@ -562,9 +555,7 @@ async def test_enabling_remote(hass, hass_ws_client, setup_api, mock_cloud_login client = await hass_ws_client(hass) cloud = hass.data[DOMAIN] - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect: + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await client.send_json({"id": 5, "type": "cloud/remote/connect"}) response = await client.receive_json() assert response["success"] @@ -578,9 +569,7 @@ async def test_disabling_remote(hass, hass_ws_client, setup_api, mock_cloud_logi client = await hass_ws_client(hass) cloud = hass.data[DOMAIN] - with patch( - "hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro() - ) as mock_disconnect: + with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect: await client.send_json({"id": 5, "type": "cloud/remote/disconnect"}) response = await client.receive_json() assert response["success"] @@ -670,9 +659,7 @@ async def test_enabling_remote_trusted_networks_other( client = await hass_ws_client(hass) cloud = hass.data[DOMAIN] - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect: + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await client.send_json({"id": 5, "type": "cloud/remote/connect"}) response = await client.receive_json() @@ -885,7 +872,7 @@ async def test_thingtalk_convert(hass, hass_ws_client, setup_api): with patch( "homeassistant.components.cloud.http_api.thingtalk.async_convert", - return_value=mock_coro({"hello": "world"}), + return_value={"hello": "world"}, ): await client.send_json( {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 10a7bc38c05..e174b080102 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,5 +1,4 @@ """Test the cloud component.""" -from unittest.mock import patch import pytest @@ -11,12 +10,12 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch async def test_constructor_loads_info_from_config(hass): """Test non-dev mode loads info from SERVERS constant.""" - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): result = await async_setup_component( hass, "cloud", @@ -63,17 +62,13 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): assert hass.services.has_service(DOMAIN, "remote_connect") assert hass.services.has_service(DOMAIN, "remote_disconnect") - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect: + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await hass.services.async_call(DOMAIN, "remote_connect", blocking=True) assert mock_connect.called assert cloud.client.remote_autostart - with patch( - "hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro() - ) as mock_disconnect: + with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect: await hass.services.async_call(DOMAIN, "remote_disconnect", blocking=True) assert mock_disconnect.called @@ -82,9 +77,9 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): # Test admin access required non_admin_context = Context(user_id=hass_read_only_user.id) - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect, pytest.raises(Unauthorized): + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect, pytest.raises( + Unauthorized + ): await hass.services.async_call( DOMAIN, "remote_connect", blocking=True, context=non_admin_context ) @@ -92,7 +87,7 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): assert mock_connect.called is False with patch( - "hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro() + "hass_nabucasa.remote.RemoteUI.disconnect" ) as mock_disconnect, pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, "remote_disconnect", blocking=True, context=non_admin_context @@ -103,7 +98,7 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): async def test_startup_shutdown_events(hass, mock_cloud_fixture): """Test if the cloud will start on startup event.""" - with patch("hass_nabucasa.Cloud.stop", return_value=mock_coro()) as mock_stop: + with patch("hass_nabucasa.Cloud.stop") as mock_stop: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -114,7 +109,7 @@ async def test_setup_existing_cloud_user(hass, hass_storage): """Test setup with API push default data.""" user = await hass.auth.async_create_system_user("Cloud test") hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": user.id}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): result = await async_setup_component( hass, "cloud", @@ -148,9 +143,7 @@ async def test_on_connect(hass, mock_cloud_fixture): assert len(hass.states.async_entity_ids("binary_sensor")) == 1 - with patch( - "homeassistant.helpers.discovery.async_load_platform", side_effect=mock_coro - ) as mock_load: + with patch("homeassistant.helpers.discovery.async_load_platform") as mock_load: await cl.iot._on_connect[-1]() await hass.async_block_till_done() diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index d1b6f9ed867..4f2d5d6d661 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -1,9 +1,9 @@ """Test Cloud preferences.""" -from unittest.mock import patch - from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences +from tests.async_mock import patch + async def test_set_username(hass): """Test we clear config if we set different username.""" diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index 9d1e89fbc24..8997bc4a5d6 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -1,11 +1,11 @@ """Tests for the CoinMarketCap sensor platform.""" import json import unittest -from unittest.mock import patch import homeassistant.components.sensor as sensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 0cb7e9293e9..f20011d4482 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -2,11 +2,11 @@ import os import tempfile import unittest -from unittest.mock import patch import homeassistant.components.notify as notify from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index b923be81888..bbf69dc73a0 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,10 +1,10 @@ """The tests for the Command line sensor platform.""" import unittest -from unittest.mock import patch from homeassistant.components.command_line import sensor as command_line from homeassistant.helpers.template import Template +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 45ffa1d08ec..1d160870169 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,11 +1,11 @@ """Test Automation config panel.""" import json -from asynctest import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.async_mock import patch + async def test_get_device_config(hass, hass_client): """Test getting device config.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 9eb9a741da0..e5da27818fc 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,7 +1,6 @@ """Test config entries API.""" from collections import OrderedDict -from unittest.mock import patch import pytest import voluptuous as vol @@ -13,10 +12,10 @@ from homeassistant.core import callback from homeassistant.generated import config_flows from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockModule, - mock_coro_func, mock_entity_platform, mock_integration, ) @@ -141,13 +140,17 @@ async def test_initialize_flow(hass, client): return self.async_show_form( step_id="user", data_schema=schema, - description_placeholders={"url": "https://example.com"}, + description_placeholders={ + "url": "https://example.com", + "show_advanced_options": self.show_advanced_options, + }, errors={"username": "Should be unique."}, ) with patch.dict(HANDLERS, {"test": TestFlow}): resp = await client.post( - "/api/config/config_entries/flow", json={"handler": "test"} + "/api/config/config_entries/flow", + json={"handler": "test", "show_advanced_options": True}, ) assert resp.status == 200 @@ -163,7 +166,10 @@ async def test_initialize_flow(hass, client): {"name": "username", "required": True, "type": "string"}, {"name": "password", "required": True, "type": "string"}, ], - "description_placeholders": {"url": "https://example.com"}, + "description_placeholders": { + "url": "https://example.com", + "show_advanced_options": True, + }, "errors": {"username": "Should be unique."}, } @@ -221,7 +227,9 @@ async def test_create_account(hass, client): """Test a flow that creates an account.""" mock_entity_platform(hass, "config_flow.test", None) - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @@ -256,7 +264,9 @@ async def test_create_account(hass, client): async def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): @@ -313,7 +323,9 @@ async def test_two_step_flow(hass, client): async def test_continue_flow_unauth(hass, client, hass_admin_user): """Test we can't finish a two step flow.""" - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): @@ -378,7 +390,12 @@ async def test_get_progress_index(hass, hass_ws_client): assert response["success"] assert response["result"] == [ - {"flow_id": form["flow_id"], "handler": "test", "context": {"source": "hassio"}} + { + "flow_id": form["flow_id"], + "handler": "test", + "step_id": "account", + "context": {"source": "hassio"}, + } ] @@ -509,7 +526,9 @@ async def test_options_flow(hass, client): async def test_two_step_options_flow(hass, client): """Test we can finish a two step options flow.""" - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) class TestFlow(core_ce.ConfigFlow): @staticmethod @@ -659,7 +678,9 @@ async def test_update_entry_nonexisting(hass, hass_ws_client): async def test_ignore_flow(hass, hass_ws_client): """Test we can ignore a flow.""" assert await async_setup_component(hass, "config", {}) - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 8caa0f3e6fb..6a7447d9409 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,6 +1,4 @@ """Test hassbian config.""" -from unittest.mock import patch - import pytest from homeassistant.bootstrap import async_setup_component @@ -9,7 +7,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location -from tests.common import mock_coro +from tests.async_mock import patch ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE @@ -31,7 +29,7 @@ async def test_validate_config_ok(hass, hass_client): with patch( "homeassistant.components.config.core.async_check_ha_config_file", - return_value=mock_coro(), + return_value=None, ): resp = await client.post("/api/config/core/check_config") @@ -42,7 +40,7 @@ async def test_validate_config_ok(hass, hass_client): with patch( "homeassistant.components.config.core.async_check_ha_config_file", - return_value=mock_coro("beer"), + return_value="beer", ): resp = await client.post("/api/config/core/check_config") @@ -60,6 +58,8 @@ async def test_websocket_core_update(hass, client): assert hass.config.location_name != "Huis" assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.time_zone.zone != "America/New_York" + assert hass.config.external_url != "https://www.example.com" + assert hass.config.internal_url != "http://example.com" await client.send_json( { @@ -71,6 +71,8 @@ async def test_websocket_core_update(hass, client): "location_name": "Huis", CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, "time_zone": "America/New_York", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", } ) @@ -85,6 +87,8 @@ async def test_websocket_core_update(hass, client): assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.external_url == "https://www.example.com" + assert hass.config.internal_url == "http://example.local" dt_util.set_default_time_zone(ORIG_TIME_ZONE) @@ -121,8 +125,7 @@ async def test_websocket_bad_core_update(hass, client): async def test_detect_config(hass, client): """Test detect config.""" with patch( - "homeassistant.util.location.async_detect_location_info", - return_value=mock_coro(None), + "homeassistant.util.location.async_detect_location_info", return_value=None, ): await client.send_json({"id": 1, "type": "config/core/detect"}) @@ -136,20 +139,18 @@ async def test_detect_config_fail(hass, client): """Test detect config.""" with patch( "homeassistant.util.location.async_detect_location_info", - return_value=mock_coro( - location.LocationInfo( - ip=None, - country_code=None, - country_name=None, - region_code=None, - region_name=None, - city=None, - zip_code=None, - latitude=None, - longitude=None, - use_metric=True, - time_zone="Europe/Amsterdam", - ) + return_value=location.LocationInfo( + ip=None, + country_code=None, + country_name=None, + region_code=None, + region_name=None, + city=None, + zip_code=None, + latitude=None, + longitude=None, + use_metric=True, + time_zone="Europe/Amsterdam", ), ): await client.send_json({"id": 1, "type": "config/core/detect"}) diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index d8c9ea19b70..30a475dab77 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,12 +1,12 @@ """Test Customize config panel.""" import json -from asynctest import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.config import DATA_CUSTOMIZE +from tests.async_mock import patch + async def test_get_entity(hass, hass_client): """Test getting entity.""" diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index a3710d48b94..c2557c83a4a 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -34,6 +34,7 @@ async def test_list_devices(hass, client, registry): manufacturer="manufacturer", model="model", via_device=("bridgeid", "0123"), + entry_type="service", ) await client.send_json({"id": 5, "type": "config/device_registry/list"}) @@ -49,6 +50,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, + "entry_type": None, "via_device_id": None, "area_id": None, "name_by_user": None, @@ -60,6 +62,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, + "entry_type": "service", "via_device_id": dev1, "area_id": None, "name_by_user": None, diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index d00e0317e9e..98ad2041713 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,12 +1,11 @@ """Test Group config panel.""" import json -from unittest.mock import patch - -from asynctest import CoroutineMock from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.async_mock import AsyncMock, patch + VIEW_NAME = "api:config:group:config" @@ -52,7 +51,7 @@ async def test_update_device_config(hass, hass_client): """Mock writing data.""" written.append(data) - mock_call = CoroutineMock() + mock_call = AsyncMock() with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 7f9b62d71f6..6dd16fef7ec 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,11 +1,11 @@ """Test config init.""" -from unittest.mock import patch from homeassistant.components import config from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.common import mock_component, mock_coro +from tests.async_mock import patch +from tests.common import mock_component async def test_config_setup(hass, loop): @@ -20,8 +20,9 @@ async def test_load_on_demand_already_loaded(hass, aiohttp_client): with patch.object(config, "SECTIONS", []), patch.object( config, "ON_DEMAND", ["zwave"] - ), patch("homeassistant.components.config.zwave.async_setup") as stp: - stp.return_value = mock_coro(True) + ), patch( + "homeassistant.components.config.zwave.async_setup", return_value=True + ) as stp: await async_setup_component(hass, "config", {}) @@ -38,8 +39,9 @@ async def test_load_on_demand_on_load(hass, aiohttp_client): assert "config.zwave" not in hass.config.components - with patch("homeassistant.components.config.zwave.async_setup") as stp: - stp.return_value = mock_coro(True) + with patch( + "homeassistant.components.config.zwave.async_setup", return_value=True + ) as stp: hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "zwave"}) await hass.async_block_till_done() diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index b51628f87ae..dcaa950f342 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,12 +1,12 @@ """Test Automation config panel.""" import json -from asynctest import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.util.yaml import dump +from tests.async_mock import patch + async def test_update_scene(hass, hass_client): """Test updating a scene.""" diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 0026729766c..4dc906e92f3 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -1,9 +1,9 @@ """Tests for config/script.""" -from unittest.mock import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.async_mock import patch + async def test_delete_script(hass, hass_client): """Test deleting a script.""" diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 3624554843a..75a66f61939 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,6 +1,5 @@ """Test Z-Wave config panel.""" import json -from unittest.mock import MagicMock, patch import pytest @@ -9,6 +8,7 @@ from homeassistant.components import config from homeassistant.components.zwave import DATA_NETWORK, const from homeassistant.const import HTTP_NOT_FOUND +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue VIEW_NAME = "api:config:zwave:device_config" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 1670e0c2485..96ab3bca543 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,7 +1,8 @@ """Fixtures for component testing.""" -from asynctest import patch import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def prevent_io(): diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index c71f308dece..49058fc183e 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,10 +1,8 @@ """Test the Coolmaster config flow.""" -from unittest.mock import patch - from homeassistant import config_entries, setup from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN -from tests.common import mock_coro +from tests.async_mock import patch def _flow_data(): @@ -27,10 +25,9 @@ async def test_form(hass): "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", return_value=[1], ), patch( - "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) + "homeassistant.components.coolmaster.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.coolmaster.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.coolmaster.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data() diff --git a/tests/components/coronavirus/conftest.py b/tests/components/coronavirus/conftest.py index 45d2a00e69d..6e49d2aa164 100644 --- a/tests/components/coronavirus/conftest.py +++ b/tests/components/coronavirus/conftest.py @@ -1,8 +1,9 @@ """Test helpers.""" -from asynctest import Mock, patch import pytest +from tests.async_mock import Mock, patch + @pytest.fixture(autouse=True) def mock_cases(): diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py new file mode 100644 index 00000000000..df8df2c4bf1 --- /dev/null +++ b/tests/components/cover/test_init.py @@ -0,0 +1,12 @@ +"""The tests for Cover.""" +import homeassistant.components.cover as cover + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomCover(cover.CoverDevice): + pass + + CustomCover() + assert "CoverDevice is deprecated, modify CustomCover" in caplog.text diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index aea78f17564..25fc8ba26f2 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,52 +1,54 @@ # pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio -from unittest.mock import patch +from aiohttp import ClientError +from aiohttp.web_exceptions import HTTPForbidden import pytest -from homeassistant import data_entry_flow -from homeassistant.components.daikin import config_flow from homeassistant.components.daikin.const import KEY_IP, KEY_MAC +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" HOST = "127.0.0.1" -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.FlowHandler() - flow.hass = hass - return flow - - @pytest.fixture def mock_daikin(): """Mock pydaikin.""" - async def mock_daikin_init(): + async def mock_daikin_factory(*args, **kwargs): """Mock the init function in pydaikin.""" - pass + return Appliance with patch("homeassistant.components.daikin.config_flow.Appliance") as Appliance: - Appliance().values.get.return_value = "AABBCCDDEEFF" - Appliance().init = mock_daikin_init + type(Appliance).mac = PropertyMock(return_value="AABBCCDDEEFF") + Appliance.factory.side_effect = mock_daikin_factory yield Appliance async def test_user(hass, mock_daikin): """Test user config.""" - flow = init_config_flow(hass) + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": SOURCE_USER}, + ) - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - result = await flow.async_step_user({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -54,35 +56,27 @@ async def test_user(hass, mock_daikin): async def test_abort_if_already_setup(hass, mock_daikin): """Test we abort if Daikin is already setup.""" - flow = init_config_flow(hass) - MockConfigEntry(domain="daikin", data={KEY_MAC: MAC}).add_to_hass(hass) + MockConfigEntry(domain="daikin", unique_id=MAC).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) - result = await flow.async_step_user({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" async def test_import(hass, mock_daikin): """Test import step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": SOURCE_IMPORT}, data={}, + ) + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - result = await flow.async_step_import({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][KEY_MAC] == MAC - - -async def test_discovery(hass, mock_daikin): - """Test discovery step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_discovery({KEY_IP: HOST, KEY_MAC: MAC}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": SOURCE_IMPORT}, data={CONF_HOST: HOST}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -90,13 +84,40 @@ async def test_discovery(hass, mock_daikin): @pytest.mark.parametrize( "s_effect,reason", - [(asyncio.TimeoutError, "device_timeout"), (Exception, "device_fail")], + [ + (asyncio.TimeoutError, "device_timeout"), + (HTTPForbidden, "forbidden"), + (ClientError, "device_fail"), + (Exception, "device_fail"), + ], ) async def test_device_abort(hass, mock_daikin, s_effect, reason): """Test device abort.""" - flow = init_config_flow(hass) - mock_daikin.side_effect = s_effect + mock_daikin.factory.side_effect = s_effect - result = await flow.async_step_user({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == reason + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": reason} + assert result["step_id"] == "user" + + +@pytest.mark.parametrize( + "source, data, unique_id", [(SOURCE_DISCOVERY, {KEY_IP: HOST, KEY_MAC: MAC}, MAC)], +) +async def test_discovery(hass, mock_daikin, source, data, unique_id): + """Test discovery/zeroconf step.""" + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": source}, data=data, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + "daikin", context={"source": source}, data=data, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 2163a809b5e..8c4038f91c6 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta import re import unittest -from unittest.mock import MagicMock, patch import forecastio from requests.exceptions import HTTPError @@ -11,7 +10,8 @@ import requests_mock from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component -from tests.common import MockDependency, get_test_home_assistant, load_fixture +from tests.async_mock import MagicMock, patch +from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { "sensor": { @@ -110,12 +110,11 @@ class TestDarkSkySetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @MockDependency("forecastio") @patch( "homeassistant.components.darksky.sensor.forecastio.load_forecast", new=load_forecastMock, ) - def test_setup_with_config(self, mock_forecastio): + def test_setup_with_config(self): """Test the platform setup with configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) @@ -129,12 +128,11 @@ class TestDarkSkySetup(unittest.TestCase): state = self.hass.states.get("sensor.dark_sky_summary") assert state is None - @MockDependency("forecastio") @patch( "homeassistant.components.darksky.sensor.forecastio.load_forecast", new=load_forecastMock, ) - def test_setup_with_language_config(self, mock_forecastio): + def test_setup_with_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE) @@ -164,12 +162,11 @@ class TestDarkSkySetup(unittest.TestCase): ) assert not response - @MockDependency("forecastio") @patch( "homeassistant.components.darksky.sensor.forecastio.load_forecast", new=load_forecastMock, ) - def test_setup_with_alerts_config(self, mock_forecastio): + def test_setup_with_alerts_config(self): """Test the platform setup with alert configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS) @@ -198,3 +195,7 @@ class TestDarkSkySetup(unittest.TestCase): assert state.attributes.get("friendly_name") == "Dark Sky Summary" state = self.hass.states.get("sensor.dark_sky_alerts") assert state.state == "2" + + state = self.hass.states.get("sensor.dark_sky_daytime_high_temperature_1d") + assert state is not None + assert state.attributes.get("device_class") == "temperature" diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index 09ffe7bdc90..f871d424db6 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -1,7 +1,6 @@ """The tests for the Dark Sky weather component.""" import re import unittest -from unittest.mock import patch import forecastio from requests.exceptions import ConnectionError @@ -11,6 +10,7 @@ from homeassistant.components import weather from homeassistant.setup import setup_component from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index c03dc72019e..ddc89295cba 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,14 +1,14 @@ """deCONZ climate platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.climate as climate from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SENSORS = { "1": { "id": "Thermostat id", diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 0eda8eb71ab..4f45e36c5b1 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,14 +1,14 @@ """deCONZ cover platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.cover as cover from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + COVERS = { "1": { "id": "Level controllable cover id", diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index dd3289dea23..fc8d4f9d1ba 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,12 +1,12 @@ """Test deCONZ remote events.""" from copy import deepcopy -from asynctest import Mock - from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.common import async_capture_events + SENSORS = { "1": { "id": "Switch 1 id", @@ -67,53 +67,40 @@ async def test_deconz_events(hass): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "100" - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) + events = async_capture_events(hass, CONF_DECONZ_EVENT) gateway.api.sensors["1"].update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 1 + assert events[0].data == { "id": "switch_1", "unique_id": "00:00:00:00:00:00:00:01", "event": 2000, } - unsub() - - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 2 + assert events[1].data == { "id": "switch_3", "unique_id": "00:00:00:00:00:00:00:03", "event": 2000, "gesture": 1, } - unsub() - - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["4"].update({"state": {"gesture": 0}}) await hass.async_block_till_done() - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 3 + assert events[2].data == { "id": "switch_4", "unique_id": "00:00:00:00:00:00:00:04", "event": 1000, "gesture": 0, } - unsub() - await gateway.async_reset() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index e8c9da42ada..9af1a3151e8 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,7 +1,6 @@ """Test deCONZ gateway.""" from copy import deepcopy -from asynctest import Mock, patch import pydeconz import pytest @@ -9,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components import deconz, ssdp from homeassistant.helpers.dispatcher import async_dispatcher_connect +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 3e2760bc632..8d9d387c91c 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -2,12 +2,12 @@ import asyncio from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index e39722fdacb..229c085916e 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,14 +1,14 @@ """deCONZ light platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.light as light from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 3593fa32355..538c849e831 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,14 +1,14 @@ """deCONZ scene platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.scene as scene from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 07985e4d9f4..e880ea1000b 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,6 +1,5 @@ """deCONZ service tests.""" -from asynctest import Mock, patch import pytest import voluptuous as vol @@ -9,6 +8,8 @@ from homeassistant.components.deconz.const import CONF_BRIDGE_ID from .test_gateway import BRIDGEID, setup_deconz_integration +from tests.async_mock import Mock, patch + GROUP = { "1": { "id": "Group 1 id", diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 6e151ebd47a..b441868859b 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,14 +1,14 @@ """deCONZ switch platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.switch as switch from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SWITCHES = { "1": { "id": "On off switch id", diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 8e0862eb0cb..00fb1c1047b 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,10 +1,10 @@ """Test the default_config init.""" -from unittest.mock import patch - import pytest from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(autouse=True) def recorder_url_mock(): diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index d46d1fdc62b..49b8e017f1a 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -1,6 +1,4 @@ """The tests for local file camera component.""" -from unittest.mock import patch - import pytest from homeassistant.components.camera import ( @@ -18,6 +16,8 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import patch + ENTITY_CAMERA = "camera.demo_camera" diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index 5d5f2fc8106..0ba3a35b891 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the demo platform.""" import unittest -from unittest.mock import patch from homeassistant.components import geo_location from homeassistant.components.demo.geo_location import ( @@ -11,6 +10,7 @@ from homeassistant.const import LENGTH_KILOMETERS from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, fire_time_changed, diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 26d0d2165e7..e663600f84f 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,5 +1,4 @@ """The tests for the Demo Media player platform.""" -from asynctest import patch import pytest import voluptuous as vol @@ -7,6 +6,7 @@ import homeassistant.components.media_player as mp from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.components.media_player import common TEST_ENTITY_ID = "media_player.walkman" diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index e30d65112e8..7c7b7fa0aa1 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,6 +1,5 @@ """The tests for the notify demo platform.""" import unittest -from unittest.mock import patch import pytest import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant from tests.components.notify import common diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index e64e7d178cd..5c9ac4205fe 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -228,7 +228,7 @@ async def test_unsupported_methods(hass): assert "spot" not in state.attributes.get(ATTR_STATUS) assert state.state == STATE_OFF - # VacuumDevice should not support start and pause methods. + # VacuumEntity should not support start and pause methods. hass.states.async_set(ENTITY_VACUUM_COMPLETE, STATE_ON) await hass.async_block_till_done() assert vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE) @@ -243,7 +243,7 @@ async def test_unsupported_methods(hass): await common.async_start(hass, ENTITY_VACUUM_COMPLETE) assert not vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE) - # StateVacuumDevice does not support on/off + # StateVacuumEntity does not support on/off await common.async_turn_on(hass, entity_id=ENTITY_VACUUM_STATE) state = hass.states.get(ENTITY_VACUUM_STATE) assert state.state != STATE_CLEANING diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index 91bc2abf94d..1547391a339 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -1,6 +1,4 @@ """The tests for the denonavr media player platform.""" -from unittest.mock import patch - import pytest from homeassistant.components import media_player @@ -8,6 +6,8 @@ from homeassistant.components.denonavr import ATTR_COMMAND, DOMAIN, SERVICE_GET_ from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PLATFORM from homeassistant.setup import async_setup_component +from tests.async_mock import patch + NAME = "fake" ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 466f07a9deb..96fee84126a 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -1,11 +1,12 @@ """The tests for the derivative sensor platform.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch + async def test_state(hass): """Test derivative sensor state.""" diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 1b51d93ae6c..23e400adac4 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from datetime import datetime -from asynctest import patch import pytest from homeassistant.components import ( @@ -23,6 +22,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 8ecc341d4d2..1a366b0d2df 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -3,9 +3,7 @@ from datetime import datetime, timedelta import json import logging import os -from unittest.mock import Mock, call -from asynctest import patch import pytest from homeassistant.components import zone @@ -29,6 +27,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, call, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/devolo_home_control/__init__.py b/tests/components/devolo_home_control/__init__.py new file mode 100644 index 00000000000..5e1e323cad8 --- /dev/null +++ b/tests/components/devolo_home_control/__init__.py @@ -0,0 +1 @@ +"""Tests for the devolo_home_control integration.""" diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py new file mode 100644 index 00000000000..aacf33b69c1 --- /dev/null +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -0,0 +1,102 @@ +"""Test the devolo_home_control config flow.""" +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.devolo_home_control.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.devolo_home_control.async_setup", return_value=True, + ) as mock_setup, patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", + return_value=True, + ), patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", + return_value=["123456"], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "home_control_url": "https://homecontrol.mydevolo.com", + "mydevolo_url": "https://www.mydevolo.com", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "devolo Home Control" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + "home_control_url": "https://homecontrol.mydevolo.com", + "mydevolo_url": "https://www.mydevolo.com", + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_credentials(hass): + """Test if we get the error message on invalid credentials.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "home_control_url": "https://homecontrol.mydevolo.com", + "mydevolo_url": "https://www.mydevolo.com", + }, + ) + + assert result["errors"] == {"base": "invalid_credentials"} + + +async def test_form_already_configured(hass): + """Test if we get the error message on already configured.""" + with patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", + return_value=["1234567"], + ), patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", + return_value=True, + ): + MockConfigEntry(domain=DOMAIN, unique_id="1234567", data={}).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + "username": "test-username", + "password": "test-password", + "home_control_url": "https://homecontrol.mydevolo.com", + "mydevolo_url": "https://www.mydevolo.com", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 5c2a71ad88f..6df59873bd1 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,12 +1,12 @@ """The tests for the Dialogflow component.""" import copy import json -from unittest.mock import Mock import pytest from homeassistant import data_entry_flow from homeassistant.components import dialogflow, intent_script +from homeassistant.config import async_process_ha_core_config from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -78,7 +78,10 @@ async def fixture(hass, aiohttp_client): }, ) - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) + result = await hass.config_entries.flow.async_init( "dialogflow", context={"source": "user"} ) diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index c5cfec50637..78eddf57c30 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,6 +1,5 @@ """Test the DirecTV config flow.""" from aiohttp import ClientError as HTTPClientError -from asynctest import patch from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_SERIAL @@ -13,6 +12,7 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.components.directv import ( HOST, MOCK_SSDP_DISCOVERY_INFO, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 8b428c1b708..7203325fbbe 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from typing import Optional -from asynctest import patch from pytest import fixture from homeassistant.components.directv.media_player import ( @@ -55,6 +54,7 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index f93d839b78c..b00f62c0e0c 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -1,6 +1,4 @@ """The tests for the DirecTV remote platform.""" -from asynctest import patch - from homeassistant.components.remote import ( ATTR_COMMAND, DOMAIN as REMOTE_DOMAIN, @@ -9,6 +7,7 @@ from homeassistant.components.remote import ( from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 86865209de3..9490707e0f6 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,7 +1,6 @@ """The tests for the discovery component.""" from unittest.mock import MagicMock -from asynctest import patch import pytest from homeassistant import config_entries @@ -9,6 +8,7 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_coro # One might consider to "mock" services, but it's easy enough to just use diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 8b49f87bd0b..7c7c034c6b4 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,13 +1,12 @@ """Test the DoorBird config flow.""" import urllib -from asynctest import MagicMock, patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, init_recorder_component VALID_CONFIG = { diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 67c6d3bc58d..895a95bef7b 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -9,15 +9,15 @@ import asyncio import datetime from decimal import Decimal from itertools import chain, repeat -from unittest.mock import DEFAULT, Mock -import asynctest import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS +import tests.async_mock +from tests.async_mock import DEFAULT, Mock from tests.common import assert_setup_component @@ -26,8 +26,8 @@ def mock_connection_factory(monkeypatch): """Mock the create functions for serial and TCP Asyncio connections.""" from dsmr_parser.clients.protocol import DSMRProtocol - transport = asynctest.Mock(spec=asyncio.Transport) - protocol = asynctest.Mock(spec=DSMRProtocol) + transport = tests.async_mock.Mock(spec=asyncio.Transport) + protocol = tests.async_mock.Mock(spec=DSMRProtocol) async def connection_factory(*args, **kwargs): """Return mocked out Asyncio classes.""" @@ -327,7 +327,7 @@ async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factor config = {"platform": "dsmr", "reconnect_interval": 0} # override the mock to have it fail the first time and succeed after - first_fail_connection_factory = asynctest.CoroutineMock( + first_fail_connection_factory = tests.async_mock.AsyncMock( return_value=(transport, protocol), side_effect=chain([TimeoutError], repeat(DEFAULT)), ) diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index b90e6120444..f72e3f481b6 100644 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -1,9 +1,8 @@ """Common functions for tests.""" -from asynctest import CoroutineMock, Mock, call, patch - from homeassistant.components import dynalite from homeassistant.helpers import entity_registry +from tests.async_mock import AsyncMock, Mock, call, patch from tests.common import MockConfigEntry ATTR_SERVICE = "service" @@ -38,7 +37,7 @@ async def create_entity_from_device(hass, device): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 938bc09f59a..ea73f75a390 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,11 +1,9 @@ """Test Dynalite bridge.""" -from asynctest import CoroutineMock, Mock, patch -from dynalite_devices_lib.const import CONF_ALL - from homeassistant.components import dynalite from homeassistant.helpers.dispatcher import async_dispatcher_connect +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -17,7 +15,7 @@ async def test_update_device(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) # Not waiting so it add the devices before registration update_device_func = mock_dyn_dev.mock_calls[1][2]["update_device_func"] @@ -29,7 +27,7 @@ async def test_update_device(hass): async_dispatcher_connect( hass, f"dynalite-update-{host}-{device.unique_id}", specific_func ) - update_device_func(CONF_ALL) + update_device_func() await hass.async_block_till_done() wide_func.assert_called_once() specific_func.assert_not_called() @@ -47,7 +45,7 @@ async def test_add_devices_then_register(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) # Not waiting so it add the devices before registration new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] @@ -80,7 +78,7 @@ async def test_register_then_add_devices(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 1a1cdc16f49..11b4d6b524c 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,11 +1,11 @@ """Test Dynalite config flow.""" -from asynctest import CoroutineMock, patch import pytest from homeassistant import config_entries from homeassistant.components import dynalite +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry @@ -69,7 +69,7 @@ async def test_existing_update(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_dyn_dev().configure.assert_called_once() diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index 8e2290a9c40..be646a1854d 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,12 +1,11 @@ """Test Dynalite __init__.""" -from asynctest import call, patch - import homeassistant.components.dynalite.const as dynalite from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component +from tests.async_mock import call, patch from tests.common import MockConfigEntry @@ -38,6 +37,7 @@ async def test_async_setup(hass): "1": { CONF_NAME: "Name1", dynalite.CONF_CHANNEL: {"4": {}}, + dynalite.CONF_PRESET: {"7": {}}, dynalite.CONF_NO_DEFAULT: True, }, "2": {CONF_NAME: "Name2"}, diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index fcd801616c9..ab11a1ad897 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -2,7 +2,6 @@ import json from unittest import mock -import asynctest from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State @@ -19,6 +18,8 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device +from tests.async_mock import patch + def _get_dyson_purecool_device(): """Return a valid device as provided by the Dyson web services.""" @@ -46,8 +47,8 @@ def _get_config(): } -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -65,8 +66,8 @@ async def test_purecool_aiq_attributes(devices, login, hass): assert attributes[dyson.ATTR_VOC] == 35 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -108,8 +109,8 @@ async def test_purecool_aiq_update_state(devices, login, hass): assert attributes[dyson.ATTR_VOC] == 55 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -124,8 +125,8 @@ async def test_purecool_component_setup_only_once(devices, login, hass): assert len(hass.data[dyson.DYSON_AIQ_DEVICES]) == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -140,8 +141,8 @@ async def test_purecool_aiq_without_discovery(devices, login, hass): assert add_entities_mock.call_count == 0 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 345eae6f553..af17d1f0ab4 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -2,7 +2,6 @@ import unittest from unittest import mock -import asynctest from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState @@ -14,6 +13,7 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -344,11 +344,11 @@ class DysonTest(unittest.TestCase): assert entity.target_temperature == 23 -@asynctest.patch( +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_device_heat_on(), _get_device_cool()], ) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) async def test_setup_component_with_parent_discovery( mocked_login, mocked_devices, hass ): diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 7801c897723..d4db6051960 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -3,7 +3,6 @@ import json import unittest from unittest import mock -import asynctest from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink @@ -28,6 +27,7 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -386,8 +386,8 @@ class DysonTest(unittest.TestCase): dyson_device.set_night_mode.assert_called_with(True) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecoollink_device()], ) @@ -404,8 +404,8 @@ async def test_purecoollink_attributes(devices, login, hass): assert attributes[ATTR_OSCILLATING] is True -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -426,8 +426,8 @@ async def test_purecool_turn_on(devices, login, hass): assert device.turn_on.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -470,8 +470,8 @@ async def test_purecool_set_speed(devices, login, hass): device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_10) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -492,8 +492,8 @@ async def test_purecool_turn_off(devices, login, hass): assert device.turn_off.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -526,8 +526,8 @@ async def test_purecool_set_dyson_speed(devices, login, hass): device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_2) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -562,8 +562,8 @@ async def test_purecool_oscillate(devices, login, hass): assert device.disable_oscillation.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -599,8 +599,8 @@ async def test_purecool_set_night_mode(devices, login, hass): assert device.disable_night_mode.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -635,8 +635,8 @@ async def test_purecool_set_auto_mode(devices, login, hass): assert device.disable_auto_mode.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -671,8 +671,8 @@ async def test_purecool_set_angle(devices, login, hass): device.enable_oscillation.assert_called_with(90, 180) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -707,8 +707,8 @@ async def test_purecool_set_flow_direction_front(devices, login, hass): assert device.disable_frontal_direction.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -743,8 +743,8 @@ async def test_purecool_set_timer(devices, login, hass): assert device.disable_sleep_timer.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -804,8 +804,8 @@ async def test_purecool_update_state(devices, login, hass): assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -865,8 +865,8 @@ async def test_purecool_update_state_filter_inv(devices, login, hass): assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 4d3d1c96101..92bd3bba9aa 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -2,7 +2,6 @@ import unittest from unittest import mock -import asynctest from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink @@ -20,6 +19,7 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -258,8 +258,8 @@ class DysonTest(unittest.TestCase): assert sensor.entity_id == "sensor.dyson_1" -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index f862539f1df..64f24ec289c 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -1,7 +1,6 @@ """Tests for the EE BrightBox device scanner.""" from datetime import datetime -from asynctest import patch from eebrightbox import EEBrightBoxException import pytest @@ -9,6 +8,8 @@ from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM from homeassistant.setup import async_setup_component +from tests.async_mock import patch + def _configure_mock_get_devices(eebrightbox_mock): eebrightbox_instance = eebrightbox_mock.return_value diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 2f701a2e146..992483529a5 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,16 +1,16 @@ """Test the Elk-M1 Control config flow.""" -from asynctest import CoroutineMock, MagicMock, PropertyMock, patch - from homeassistant import config_entries, setup from homeassistant.components.elkm1.const import DOMAIN +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch + def mock_elk(invalid_auth=None, sync_complete=None): """Mock m1lib Elk.""" mocked_elk = MagicMock() type(mocked_elk).invalid_auth = PropertyMock(return_value=invalid_auth) - type(mocked_elk).sync_complete = CoroutineMock() + type(mocked_elk).sync_complete = AsyncMock() return mocked_elk diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 052228e7aab..fa97cd2f417 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -2,7 +2,6 @@ from datetime import timedelta from ipaddress import ip_address import json -from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import pytest @@ -27,6 +26,7 @@ from homeassistant.components.emulated_hue.hue_api import ( HUE_API_USERNAME, HueAllGroupsStateView, HueAllLightsStateView, + HueConfigView, HueFullStateView, HueOneLightChangeView, HueOneLightStateView, @@ -35,6 +35,8 @@ from homeassistant.components.emulated_hue.hue_api import ( from homeassistant.const import ( ATTR_ENTITY_ID, HTTP_NOT_FOUND, + HTTP_OK, + HTTP_UNAUTHORIZED, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -42,6 +44,7 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( async_fire_time_changed, async_mock_service, @@ -54,6 +57,26 @@ BRIDGE_SERVER_PORT = get_test_instance_port() BRIDGE_URL_BASE = f"http://127.0.0.1:{BRIDGE_SERVER_PORT}" + "{}" JSON_HEADERS = {CONTENT_TYPE: const.CONTENT_TYPE_JSON} +ENTITY_IDS_BY_NUMBER = { + "1": "light.ceiling_lights", + "2": "light.bed_light", + "3": "script.set_kitchen_light", + "4": "light.kitchen_lights", + "5": "media_player.living_room", + "6": "media_player.bedroom", + "7": "media_player.walkman", + "8": "media_player.lounge_room", + "9": "fan.living_room_fan", + "10": "fan.ceiling_fan", + "11": "cover.living_room_window", + "12": "climate.hvac", + "13": "climate.heatpump", + "14": "climate.ecobee", + "15": "light.no_brightness", +} + +ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} + @pytest.fixture def hass_hue(loop, hass): @@ -144,7 +167,6 @@ def hue_client(loop, hass_hue, aiohttp_client): config = Config( None, { - emulated_hue.CONF_TYPE: emulated_hue.TYPE_ALEXA, emulated_hue.CONF_ENTITIES: { "light.bed_light": {emulated_hue.CONF_ENTITY_HIDDEN: True}, # Kitchen light is explicitly excluded from being exposed @@ -162,6 +184,7 @@ def hue_client(loop, hass_hue, aiohttp_client): }, }, ) + config.numbers = ENTITY_IDS_BY_NUMBER HueUsernameView().register(web_app, web_app.router) HueAllLightsStateView(config).register(web_app, web_app.router) @@ -169,6 +192,7 @@ def hue_client(loop, hass_hue, aiohttp_client): HueOneLightChangeView(config).register(web_app, web_app.router) HueAllGroupsStateView(config).register(web_app, web_app.router) HueFullStateView(config).register(web_app, web_app.router) + HueConfigView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) @@ -177,7 +201,7 @@ async def test_discover_lights(hue_client): """Test the discovery of lights.""" result = await hue_client.get("/api/username/lights") - assert result.status == 200 + assert result.status == HTTP_OK assert "application/json" in result.headers["content-type"] result_json = await result.json() @@ -204,7 +228,7 @@ async def test_discover_lights(hue_client): async def test_light_without_brightness_supported(hass_hue, hue_client): """Test that light without brightness is supported.""" light_without_brightness_json = await perform_get_light_state( - hue_client, "light.no_brightness", 200 + hue_client, "light.no_brightness", HTTP_OK ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True @@ -223,7 +247,7 @@ async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): ) no_brightness_result_json = await no_brightness_result.json() - assert no_brightness_result.status == 200 + assert no_brightness_result.status == HTTP_OK assert "application/json" in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 @@ -255,7 +279,7 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client): no_brightness_result_json = await no_brightness_result.json() - assert no_brightness_result.status == 200 + assert no_brightness_result.status == HTTP_OK assert "application/json" in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 @@ -283,7 +307,7 @@ async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): hass_hue.states.async_set(entity_id, state) - state_json = await perform_get_light_state(hue_client, entity_id, 200) + state_json = await perform_get_light_state(hue_client, entity_id, HTTP_OK) assert state_json["state"]["reachable"] == is_reachable, state_json @@ -292,7 +316,7 @@ async def test_discover_full_state(hue_client): """Test the discovery of full state.""" result = await hue_client.get(f"/api/{HUE_API_USERNAME}") - assert result.status == 200 + assert result.status == HTTP_OK assert "application/json" in result.headers["content-type"] result_json = await result.json() @@ -308,7 +332,7 @@ async def test_discover_full_state(hue_client): # Make sure array is correct size assert len(result_json) == 2 - assert len(config_json) == 4 + assert len(config_json) == 6 assert len(lights_json) >= 1 # Make sure the config wrapper added to the config is there @@ -319,6 +343,10 @@ async def test_discover_full_state(hue_client): assert "swversion" in config_json assert "01003542" in config_json["swversion"] + # Make sure the api version is correct + assert "apiversion" in config_json + assert "1.17.0" in config_json["apiversion"] + # Make sure the correct username in config assert "whitelist" in config_json assert HUE_API_USERNAME in config_json["whitelist"] @@ -329,6 +357,49 @@ async def test_discover_full_state(hue_client): assert "ipaddress" in config_json assert "127.0.0.1:8300" in config_json["ipaddress"] + # Make sure the device announces a link button + assert "linkbutton" in config_json + assert config_json["linkbutton"] is True + + +async def test_discover_config(hue_client): + """Test the discovery of configuration.""" + result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config") + + assert result.status == 200 + assert "application/json" in result.headers["content-type"] + + config_json = await result.json() + + # Make sure array is correct size + assert len(config_json) == 6 + + # Make sure the config wrapper added to the config is there + assert "mac" in config_json + assert "00:00:00:00:00:00" in config_json["mac"] + + # Make sure the correct version in config + assert "swversion" in config_json + assert "01003542" in config_json["swversion"] + + # Make sure the api version is correct + assert "apiversion" in config_json + assert "1.17.0" in config_json["apiversion"] + + # Make sure the correct username in config + assert "whitelist" in config_json + assert HUE_API_USERNAME in config_json["whitelist"] + assert "name" in config_json["whitelist"][HUE_API_USERNAME] + assert "HASS BRIDGE" in config_json["whitelist"][HUE_API_USERNAME]["name"] + + # Make sure the correct ip in config + assert "ipaddress" in config_json + assert "127.0.0.1:8300" in config_json["ipaddress"] + + # Make sure the device announces a link button + assert "linkbutton" in config_json + assert config_json["linkbutton"] is True + async def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" @@ -344,7 +415,9 @@ async def test_get_light_state(hass_hue, hue_client): blocking=True, ) - office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) + office_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTP_OK + ) assert office_json["state"][HUE_API_STATE_ON] is True assert office_json["state"][HUE_API_STATE_BRI] == 127 @@ -354,13 +427,18 @@ async def test_get_light_state(hass_hue, hue_client): # Check all lights view result = await hue_client.get("/api/username/lights") - assert result.status == 200 + assert result.status == HTTP_OK assert "application/json" in result.headers["content-type"] result_json = await result.json() - assert "light.ceiling_lights" in result_json - assert result_json["light.ceiling_lights"]["state"][HUE_API_STATE_BRI] == 127 + assert ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] in result_json + assert ( + result_json[ENTITY_NUMBERS_BY_ID["light.ceiling_lights"]]["state"][ + HUE_API_STATE_BRI + ] + == 127 + ) # Turn office light off await hass_hue.services.async_call( @@ -370,7 +448,9 @@ async def test_get_light_state(hass_hue, hue_client): blocking=True, ) - office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) + office_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTP_OK + ) assert office_json["state"][HUE_API_STATE_ON] is False # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 @@ -378,10 +458,10 @@ async def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - await perform_get_light_state(hue_client, "light.bed_light", 401) + await perform_get_light_state(hue_client, "light.bed_light", HTTP_UNAUTHORIZED) # Make sure kitchen light isn't accessible - await perform_get_light_state(hue_client, "light.kitchen_lights", 401) + await perform_get_light_state(hue_client, "light.kitchen_lights", HTTP_UNAUTHORIZED) async def test_put_light_state(hass, hass_hue, hue_client): @@ -432,7 +512,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", 200 + hue_client, "light.ceiling_lights", HTTP_OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 123 assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -451,7 +531,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", 200 + hue_client, "light.ceiling_lights", HTTP_OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -464,7 +544,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_result_json = await ceiling_result.json() - assert ceiling_result.status == 200 + assert ceiling_result.status == HTTP_OK assert "application/json" in ceiling_result.headers["content-type"] assert len(ceiling_result_json) == 1 @@ -473,7 +553,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_lights = hass_hue.states.get("light.ceiling_lights") assert ceiling_lights.state == STATE_OFF ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", 200 + hue_client, "light.ceiling_lights", HTTP_OK ) # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 @@ -483,13 +563,13 @@ async def test_put_light_state(hass, hass_hue, hue_client): bedroom_result = await perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == 401 + assert bedroom_result.status == HTTP_UNAUTHORIZED # Make sure we can't change the kitchen light state kitchen_result = await perform_put_light_state( - hass_hue, hue_client, "light.kitchen_light", True + hass_hue, hue_client, "light.kitchen_lights", True ) - assert kitchen_result.status == HTTP_NOT_FOUND + assert kitchen_result.status == HTTP_UNAUTHORIZED async def test_put_light_state_script(hass, hass_hue, hue_client): @@ -512,7 +592,7 @@ async def test_put_light_state_script(hass, hass_hue, hue_client): script_result_json = await script_result.json() - assert script_result.status == 200 + assert script_result.status == HTTP_OK assert len(script_result_json) == 2 kitchen_light = hass_hue.states.get("light.kitchen_lights") @@ -535,7 +615,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): hvac_result_json = await hvac_result.json() - assert hvac_result.status == 200 + assert hvac_result.status == HTTP_OK assert len(hvac_result_json) == 2 hvac = hass_hue.states.get("climate.hvac") @@ -546,7 +626,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): ecobee_result = await perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == 401 + assert ecobee_result.status == HTTP_UNAUTHORIZED async def test_put_light_state_media_player(hass_hue, hue_client): @@ -569,7 +649,7 @@ async def test_put_light_state_media_player(hass_hue, hue_client): mp_result_json = await mp_result.json() - assert mp_result.status == 200 + assert mp_result.status == HTTP_OK assert len(mp_result_json) == 2 walkman = hass_hue.states.get("media_player.walkman") @@ -604,7 +684,7 @@ async def test_close_cover(hass_hue, hue_client): hass_hue, hue_client, cover_id, True, 100 ) - assert cover_result.status == 200 + assert cover_result.status == HTTP_OK assert "application/json" in cover_result.headers["content-type"] for _ in range(7): @@ -624,6 +704,7 @@ async def test_close_cover(hass_hue, hue_client): async def test_set_position_cover(hass_hue, hue_client): """Test setting position cover .""" cover_id = "cover.living_room_window" + cover_number = ENTITY_NUMBERS_BY_ID[cover_id] # Turn the office light off first await hass_hue.services.async_call( cover.DOMAIN, @@ -651,19 +732,14 @@ async def test_set_position_cover(hass_hue, hue_client): hass_hue, hue_client, cover_id, False, brightness ) - assert cover_result.status == 200 + assert cover_result.status == HTTP_OK assert "application/json" in cover_result.headers["content-type"] cover_result_json = await cover_result.json() assert len(cover_result_json) == 2 - assert True, cover_result_json[0]["success"][ - "/lights/cover.living_room_window/state/on" - ] - assert ( - cover_result_json[1]["success"]["/lights/cover.living_room_window/state/bri"] - == level - ) + assert True, cover_result_json[0]["success"][f"/lights/{cover_number}/state/on"] + assert cover_result_json[1]["success"][f"/lights/{cover_number}/state/bri"] == level for _ in range(100): future = dt_util.utcnow() + timedelta(seconds=1) @@ -696,7 +772,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): fan_result_json = await fan_result.json() - assert fan_result.status == 200 + assert fan_result.status == HTTP_OK assert len(fan_result_json) == 2 living_room_fan = hass_hue.states.get("fan.living_room_fan") @@ -707,6 +783,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): # pylint: disable=invalid-name async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): """Test the form with urlencoded content.""" + entity_number = ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] # Needed for Alexa await perform_put_test_on_ceiling_lights( hass_hue, hue_client, "application/x-www-form-urlencoded" @@ -715,7 +792,7 @@ async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): # Make sure we fail gracefully when we can't parse the data data = {"key1": "value1", "key2": "value2"} result = await hue_client.put( - "/api/username/lights/light.ceiling_lights/state", + f"/api/username/lights/{entity_number}/state", headers={"content-type": "application/x-www-form-urlencoded"}, data=data, ) @@ -725,22 +802,26 @@ async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): async def test_entity_not_found(hue_client): """Test for entity which are not found.""" - result = await hue_client.get("/api/username/lights/not.existant_entity") + result = await hue_client.get("/api/username/lights/98") assert result.status == HTTP_NOT_FOUND - result = await hue_client.put("/api/username/lights/not.existant_entity/state") + result = await hue_client.put("/api/username/lights/98/state") assert result.status == HTTP_NOT_FOUND async def test_allowed_methods(hue_client): """Test the allowed methods.""" - result = await hue_client.get("/api/username/lights/light.ceiling_lights/state") + result = await hue_client.get( + "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]/state" + ) assert result.status == 405 - result = await hue_client.put("/api/username/lights/light.ceiling_lights") + result = await hue_client.put( + "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]" + ) assert result.status == 405 @@ -753,7 +834,9 @@ async def test_proper_put_state_request(hue_client): """Test the request to set the state.""" # Test proper on value parsing result = await hue_client.put( - "/api/username/lights/{}/state".format("light.ceiling_lights"), + "/api/username/lights/{}/state".format( + ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] + ), data=json.dumps({HUE_API_STATE_ON: 1234}), ) @@ -761,7 +844,9 @@ async def test_proper_put_state_request(hue_client): # Test proper brightness value parsing result = await hue_client.put( - "/api/username/lights/{}/state".format("light.ceiling_lights"), + "/api/username/lights/{}/state".format( + ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] + ), data=json.dumps({HUE_API_STATE_ON: True, HUE_API_STATE_BRI: "Hello world!"}), ) @@ -773,7 +858,7 @@ async def test_get_empty_groups_state(hue_client): # Test proper on value parsing result = await hue_client.get("/api/username/groups") - assert result.status == 200 + assert result.status == HTTP_OK result_json = await result.json() @@ -801,7 +886,7 @@ async def perform_put_test_on_ceiling_lights( hass_hue, hue_client, "light.ceiling_lights", True, 56, content_type ) - assert office_result.status == 200 + assert office_result.status == HTTP_OK assert "application/json" in office_result.headers["content-type"] office_result_json = await office_result.json() @@ -816,11 +901,12 @@ async def perform_put_test_on_ceiling_lights( async def perform_get_light_state(client, entity_id, expected_status): """Test the getting of a light state.""" - result = await client.get(f"/api/username/lights/{entity_id}") + entity_number = ENTITY_NUMBERS_BY_ID[entity_id] + result = await client.get(f"/api/username/lights/{entity_number}") assert result.status == expected_status - if expected_status == 200: + if expected_status == HTTP_OK: assert "application/json" in result.headers["content-type"] return await result.json() @@ -850,8 +936,9 @@ async def perform_put_light_state( if saturation is not None: data[HUE_API_STATE_SAT] = saturation + entity_number = ENTITY_NUMBERS_BY_ID[entity_id] result = await client.put( - f"/api/username/lights/{entity_id}/state", + f"/api/username/lights/{entity_number}/state", headers=req_headers, data=json.dumps(data).encode(), ) @@ -867,6 +954,7 @@ async def test_external_ip_blocked(hue_client): getUrls = [ "/api/username/groups", "/api/username", + "/api/username/config", "/api/username/lights", "/api/username/lights/light.ceiling_lights", ] @@ -878,12 +966,26 @@ async def test_external_ip_blocked(hue_client): ): for getUrl in getUrls: result = await hue_client.get(getUrl) - assert result.status == 401 + assert result.status == HTTP_UNAUTHORIZED for postUrl in postUrls: result = await hue_client.post(postUrl) - assert result.status == 401 + assert result.status == HTTP_UNAUTHORIZED for putUrl in putUrls: result = await hue_client.put(putUrl) - assert result.status == 401 + assert result.status == HTTP_UNAUTHORIZED + + +async def test_unauthorized_user_blocked(hue_client): + """Test unauthorized_user blocked.""" + getUrls = [ + "/api/wronguser", + "/api/wronguser/config", + ] + for getUrl in getUrls: + result = await hue_client.get(getUrl) + assert result.status == HTTP_OK + + result_json = await result.json() + assert result_json[0]["error"]["description"] == "unauthorized user" diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 6fa6d969539..b1cf2aacb1b 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,8 +1,8 @@ """Test the Emulated Hue component.""" -from unittest.mock import MagicMock, Mock, patch - from homeassistant.components.emulated_hue import Config +from tests.async_mock import MagicMock, Mock, patch + def test_config_google_home_entity_id_to_number(): """Test config adheres to the type.""" diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 889f6437b0a..32859ca00c1 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,7 +1,6 @@ """The tests for the emulated Hue component.""" import json import unittest -from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import defusedxml.ElementTree as ET @@ -9,7 +8,9 @@ import requests from homeassistant import const, setup from homeassistant.components import emulated_hue +from homeassistant.const import HTTP_OK +from tests.async_mock import patch from tests.common import get_test_home_assistant, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() @@ -51,12 +52,14 @@ class TestEmulatedHue(unittest.TestCase): """Test the description.""" result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) - assert result.status_code == 200 + assert result.status_code == HTTP_OK assert "text/xml" in result.headers["content-type"] # Make sure the XML is parsable try: - ET.fromstring(result.text) + root = ET.fromstring(result.text) + ns = {"s": "urn:schemas-upnp-org:device-1-0"} + assert root.find("./s:device/s:serialNumber", ns).text == "001788FFFE23BFC2" except: # noqa: E722 pylint: disable=bare-except self.fail("description.xml is not valid XML!") @@ -68,7 +71,7 @@ class TestEmulatedHue(unittest.TestCase): BRIDGE_URL_BASE.format("/api"), data=json.dumps(request_json), timeout=5 ) - assert result.status_code == 200 + assert result.status_code == HTTP_OK assert "application/json" in result.headers["content-type"] resp_json = result.json() @@ -87,7 +90,7 @@ class TestEmulatedHue(unittest.TestCase): timeout=5, ) - assert result.status_code == 200 + assert result.status_code == HTTP_OK assert "application/json" in result.headers["content-type"] resp_json = result.json() diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 53b6217fcbc..5ff29194adf 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -1,6 +1,4 @@ """Tests for emulated_roku library bindings.""" -from unittest.mock import Mock, patch - from homeassistant.components.emulated_roku.binding import ( ATTR_APP_ID, ATTR_COMMAND_TYPE, @@ -14,7 +12,7 @@ from homeassistant.components.emulated_roku.binding import ( EmulatedRoku, ) -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock, Mock, patch async def test_events_fired_properly(hass): @@ -39,7 +37,7 @@ async def test_events_fired_properly(hass): nonlocal roku_event_handler roku_event_handler = handler - return Mock(start=mock_coro_func(), close=mock_coro_func()) + return Mock(start=AsyncMock(), close=AsyncMock()) def listener(event): events.append(event) diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index efdf330a876..92952a5d840 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,17 +1,15 @@ """Test emulated_roku component setup process.""" -from unittest.mock import Mock, patch - from homeassistant.components import emulated_roku from homeassistant.setup import async_setup_component -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock, Mock, patch async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" with patch.object(emulated_roku, "configured_servers", return_value=[]), patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ): assert ( await async_setup_component( @@ -36,7 +34,7 @@ async def test_config_already_registered_not_configured(hass): """Test that an already registered name causes the entry to be ignored.""" with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ) as instantiate, patch.object( emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] ): @@ -75,7 +73,7 @@ async def test_setup_entry_successful(hass): with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ) as instantiate: assert await emulated_roku.async_setup_entry(hass, entry) is True @@ -99,7 +97,7 @@ async def test_unload_entry(hass): with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ): assert await emulated_roku.async_setup_entry(hass, entry) is True diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4b951f9a369..50aaa989028 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,12 +1,18 @@ """Test config flow.""" from collections import namedtuple -from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.esphome import DATA_KEY, config_flow +from homeassistant.components.esphome import DATA_KEY +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, MagicMock, patch +from tests.common import MockConfigEntry MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) @@ -24,8 +30,8 @@ def mock_client(): return mock_client mock_client.side_effect = mock_constructor - mock_client.connect.return_value = mock_coro() - mock_client.disconnect.return_value = mock_coro() + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() yield mock_client @@ -40,26 +46,27 @@ def mock_api_connection_error(): yield mock_error -def _setup_flow_handler(hass): - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass - flow.context = {} - return flow - - async def test_user_connection_works(hass, mock_client): """Test we can finish a config flow.""" - flow = _setup_flow_handler(hass) - result = await flow.async_step_user(user_input=None) - assert result["type"] == "form" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "user"}, data=None, + ) - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(False, "test")) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" - result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 80}) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) - assert result["type"] == "create_entry" - assert result["data"] == {"host": "127.0.0.1", "port": 80, "password": ""} + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_HOST: "127.0.0.1", CONF_PORT: 80, CONF_PASSWORD: ""} assert result["title"] == "test" + assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 assert len(mock_client.disconnect.mock_calls) == 1 @@ -70,8 +77,6 @@ async def test_user_connection_works(hass, mock_client): async def test_user_resolve_error(hass, mock_api_connection_error, mock_client): """Test user step with IP resolve error.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) class MockResolveError(mock_api_connection_error): """Create an exception with a specific error message.""" @@ -85,13 +90,16 @@ async def test_user_resolve_error(hass, mock_api_connection_error, mock_client): new_callable=lambda: MockResolveError, ) as exc: mock_client.device_info.side_effect = exc - result = await flow.async_step_user( - user_input={"host": "127.0.0.1", "port": 6053} + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} + assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 assert len(mock_client.disconnect.mock_calls) == 1 @@ -99,16 +107,18 @@ async def test_user_resolve_error(hass, mock_api_connection_error, mock_client): async def test_user_connection_error(hass, mock_api_connection_error, mock_client): """Test user step with connection error.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) - mock_client.device_info.side_effect = mock_api_connection_error - result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} + assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 assert len(mock_client.disconnect.mock_calls) == 1 @@ -116,125 +126,159 @@ async def test_user_connection_error(hass, mock_api_connection_error, mock_clien async def test_user_with_password(hass, mock_client): """Test user step with password.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(True, "test")) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) - result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) - - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - result = await flow.async_step_authenticate(user_input={"password": "password1"}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password1"} + ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - "host": "127.0.0.1", - "port": 6053, - "password": "password1", + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "password1", } assert mock_client.password == "password1" async def test_user_invalid_password(hass, mock_api_connection_error, mock_client): """Test user step with invalid password.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(True, "test")) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" - await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) mock_client.connect.side_effect = mock_api_connection_error - result = await flow.async_step_authenticate(user_input={"password": "invalid"}) - assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "invalid"} + ) + + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_password"} async def test_discovery_initiation(hass, mock_client): """Test discovery importing works.""" - flow = _setup_flow_handler(hass) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) + service_info = { "host": "192.168.43.183", "port": 6053, "hostname": "test8266.local.", "properties": {}, } + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(False, "test8266")) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "form" - assert result["step_id"] == "discovery_confirm" - assert result["description_placeholders"]["name"] == "test8266" - assert flow.context["title_placeholders"]["name"] == "test8266" - - result = await flow.async_step_discovery_confirm(user_input={}) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "test8266" - assert result["data"]["host"] == "test8266.local" - assert result["data"]["port"] == 6053 + assert result["data"][CONF_HOST] == "192.168.43.183" + assert result["data"][CONF_PORT] == 6053 + + assert result["result"] + assert result["result"].unique_id == "test8266" async def test_discovery_already_configured_hostname(hass, mock_client): """Test discovery aborts if already configured via hostname.""" - MockConfigEntry( - domain="esphome", data={"host": "test8266.local", "port": 6053, "password": ""} - ).add_to_hass(hass) + entry = MockConfigEntry( + domain="esphome", + data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + + entry.add_to_hass(hass) - flow = _setup_flow_handler(hass) service_info = { "host": "192.168.43.183", "port": 6053, "hostname": "test8266.local.", "properties": {}, } - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert entry.unique_id == "test8266" + async def test_discovery_already_configured_ip(hass, mock_client): """Test discovery aborts if already configured via static IP.""" - MockConfigEntry( - domain="esphome", data={"host": "192.168.43.183", "port": 6053, "password": ""} - ).add_to_hass(hass) + entry = MockConfigEntry( + domain="esphome", + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + + entry.add_to_hass(hass) - flow = _setup_flow_handler(hass) service_info = { "host": "192.168.43.183", "port": 6053, "hostname": "test8266.local.", "properties": {"address": "192.168.43.183"}, } - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert entry.unique_id == "test8266" + async def test_discovery_already_configured_name(hass, mock_client): """Test discovery aborts if already configured via name.""" entry = MockConfigEntry( - domain="esphome", data={"host": "192.168.43.183", "port": 6053, "password": ""} + domain="esphome", + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, ) entry.add_to_hass(hass) + mock_entry_data = MagicMock() mock_entry_data.device_info.name = "test8266" hass.data[DATA_KEY] = {entry.entry_id: mock_entry_data} - flow = _setup_flow_handler(hass) service_info = { - "host": "192.168.43.183", + "host": "192.168.43.184", "port": 6053, "hostname": "test8266.local.", "properties": {"address": "test8266.local"}, } - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.184" + async def test_discovery_duplicate_data(hass, mock_client): """Test discovery aborts if same mDNS packet arrives.""" @@ -245,16 +289,41 @@ async def test_discovery_duplicate_data(hass, mock_client): "properties": {"address": "test8266.local"}, } - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(False, "test8266")) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": "zeroconf"} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": "zeroconf"} ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +async def test_discovery_updates_unique_id(hass, mock_client): + """Test a duplicate discovery host aborts and updates existing entry.""" + entry = MockConfigEntry( + domain="esphome", + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + + entry.add_to_hass(hass) + + service_info = { + "host": "192.168.43.183", + "port": 6053, + "hostname": "test8266.local.", + "properties": {"address": "test8266.local"}, + } + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index 8506cd2d817..b40211d5a91 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -1,6 +1,4 @@ """The tests for the facebox component.""" -from unittest.mock import Mock, mock_open, patch - import pytest import requests import requests_mock @@ -23,6 +21,8 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, mock_open, patch + MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index fc8bfc318bb..b164cc93f2e 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -1,6 +1,5 @@ """The tests for local file sensor platform.""" import unittest -from unittest.mock import Mock, patch from mock_open import MockOpen @@ -12,6 +11,7 @@ from homeassistant.components.fail2ban.sensor import ( ) from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index d9935cc0063..f27a3ff8ab6 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -31,7 +31,7 @@ class TestFanEntity(unittest.TestCase): assert self.fan.state == "off" assert len(self.fan.speed_list) == 0 assert self.fan.supported_features == 0 - assert {"speed_list": []} == self.fan.capability_attributes + assert self.fan.capability_attributes == {} # Test set_speed not required self.fan.oscillate(True) with pytest.raises(NotImplementedError): diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 58a660fcb5d..823bdf6eb63 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -6,7 +6,6 @@ from os.path import exists import time import unittest from unittest import mock -from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import ( @@ -22,6 +21,7 @@ from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 3c6a2fbb92d..4187fe561cc 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,6 +1,4 @@ """The tests for Home Assistant ffmpeg.""" -from unittest.mock import MagicMock - import homeassistant.components.ffmpeg as ffmpeg from homeassistant.components.ffmpeg import ( DOMAIN, @@ -12,6 +10,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import MagicMock from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 9a316a85735..b57232d25ad 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -1,11 +1,11 @@ """The test for the fido sensor platform.""" import logging import sys -from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido +from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component CONTRACT = "123456789" diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index bd5ae68cb37..e4ae125949a 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -1,13 +1,13 @@ """The tests for the notify file platform.""" import os import unittest -from unittest.mock import call, mock_open, patch import homeassistant.components.notify as notify from homeassistant.components.notify import ATTR_TITLE_DEFAULT from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import call, mock_open, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 3afdd8284fc..416f3e8c721 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -1,6 +1,5 @@ """The tests for local file sensor platform.""" import unittest -from unittest.mock import Mock, patch # Using third party package because of a bug reading binary data in Python 3.4 # https://bugs.python.org/issue23004 @@ -9,6 +8,7 @@ from mock_open import MockOpen from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant, mock_registry diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 06bf7cfaf12..238cb366f73 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,7 +1,6 @@ """The test for the data filter sensor platform.""" from datetime import timedelta import unittest -from unittest.mock import patch from homeassistant.components.filter.sensor import ( LowPassFilter, @@ -15,6 +14,7 @@ import homeassistant.core as ha from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, get_test_home_assistant, diff --git a/tests/components/flick_electric/__init__.py b/tests/components/flick_electric/__init__.py new file mode 100644 index 00000000000..7ba25e6c180 --- /dev/null +++ b/tests/components/flick_electric/__init__.py @@ -0,0 +1 @@ +"""Tests for the Flick Electric integration.""" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py new file mode 100644 index 00000000000..94bff11135a --- /dev/null +++ b/tests/components/flick_electric/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Flick Electric config flow.""" +import asyncio + +from asynctest import patch +from pyflick.authentication import AuthException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.flick_electric.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +CONF = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} + + +async def _flow_submit(hass): + return await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONF, + ) + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", + return_value="123456789abcdef", + ), patch( + "homeassistant.components.flick_electric.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.flick_electric.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONF, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Flick Electric: test-username" + assert result2["data"] == CONF + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicate_login(hass): + """Test uniqueness of username.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF, + title="Flick Electric: test-username", + unique_id="flick_electric_test-username", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", + return_value="123456789abcdef", + ): + result = await _flow_submit(hass) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", + side_effect=AuthException, + ): + result = await _flow_submit(hass) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + with patch( + "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", + side_effect=asyncio.TimeoutError, + ): + result = await _flow_submit(hass) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_generic_exception(hass): + """Test we handle cannot connect error.""" + with patch( + "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", + side_effect=Exception, + ): + result = await _flow_submit(hass) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 6ce3391c2c6..a408a652f32 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -1,10 +1,11 @@ """Test the flume config flow.""" -from asynctest import MagicMock, patch import requests.exceptions from homeassistant import config_entries, setup from homeassistant.components.flume.const import DOMAIN +from tests.async_mock import MagicMock, patch + def _get_mocked_flume_device_list(): flume_device_list_mock = MagicMock() diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 21fcb4798db..a3a0d41e885 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the flunearyou config flow.""" -from asynctest import patch from pyflunearyou.errors import FluNearYouError from homeassistant import data_entry_flow @@ -7,6 +6,7 @@ from homeassistant.components.flunearyou import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index c0befe8e69c..ed16e94a283 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1,5 +1,4 @@ """The tests for the Flux switch platform.""" -from asynctest.mock import patch import pytest from homeassistant.components import light, switch @@ -14,6 +13,7 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 0702e64b4f8..997bac23f22 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,11 +1,10 @@ """The tests for the folder_watcher component.""" import os -from unittest.mock import Mock, patch from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component -from tests.common import MockDependency +from tests.async_mock import Mock, patch async def test_invalid_path_setup(hass): @@ -29,8 +28,7 @@ async def test_valid_path_setup(hass): ) -@MockDependency("watchdog", "events") -def test_event(mock_watchdog): +def test_event(): """Check that Home Assistant events are fired correctly on watchdog event.""" class MockPatternMatchingEventHandler: @@ -39,17 +37,20 @@ def test_event(mock_watchdog): def __init__(self, patterns): pass - mock_watchdog.events.PatternMatchingEventHandler = MockPatternMatchingEventHandler - hass = Mock() - handler = folder_watcher.create_event_handler(["*"], hass) - handler.on_created( - Mock(is_directory=False, src_path="/hello/world.txt", event_type="created") - ) - assert hass.bus.fire.called - assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN - assert hass.bus.fire.mock_calls[0][1][1] == { - "event_type": "created", - "path": "/hello/world.txt", - "file": "world.txt", - "folder": "/hello", - } + with patch( + "homeassistant.components.folder_watcher.PatternMatchingEventHandler", + MockPatternMatchingEventHandler, + ): + hass = Mock() + handler = folder_watcher.create_event_handler(["*"], hass) + handler.on_created( + Mock(is_directory=False, src_path="/hello/world.txt", event_type="created") + ) + assert hass.bus.fire.called + assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN + assert hass.bus.fire.mock_calls[0][1][1] == { + "event_type": "created", + "path": "/hello/world.txt", + "file": "world.txt", + "folder": "/hello", + } diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 37a0310d163..0a145c88479 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -2,7 +2,6 @@ import asyncio import re -from unittest.mock import MagicMock import pytest @@ -20,6 +19,7 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock from tests.common import load_fixture VALID_CONFIG = { diff --git a/tests/components/forked_daapd/__init__.py b/tests/components/forked_daapd/__init__.py new file mode 100644 index 00000000000..51b3c619e5d --- /dev/null +++ b/tests/components/forked_daapd/__init__.py @@ -0,0 +1 @@ +"""Tests for the forked_daapd component.""" diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py new file mode 100644 index 00000000000..3dc62bae8bd --- /dev/null +++ b/tests/components/forked_daapd/test_config_flow.py @@ -0,0 +1,204 @@ +"""The config flow tests for the forked_daapd media player platform.""" +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.forked_daapd.const import ( + CONF_LIBRESPOT_JAVA_PORT, + CONF_MAX_PLAYLISTS, + CONF_TTS_PAUSE_TIME, + CONF_TTS_VOLUME, + DOMAIN, +) +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_PUSH, + SOURCE_USER, + SOURCE_ZEROCONF, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +SAMPLE_CONFIG = { + "websocket_port": 3688, + "version": "25.0", + "buildoptions": [ + "ffmpeg", + "iTunes XML", + "Spotify", + "LastFM", + "MPD", + "Device verification", + "Websockets", + "ALSA", + ], +} + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create hass config_entry fixture.""" + data = { + CONF_HOST: "192.168.1.1", + CONF_PORT: "2345", + CONF_PASSWORD: "", + } + return MockConfigEntry( + version=1, + domain=DOMAIN, + title="", + data=data, + options={}, + system_options={}, + source=SOURCE_USER, + connection_class=CONN_CLASS_LOCAL_PUSH, + entry_id=1, + ) + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + +async def test_config_flow(hass, config_entry): + """Test that the user step works.""" + with patch( + "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection" + ) as mock_test_connection, patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI.get_request", + autospec=True, + ) as mock_get_request: + mock_get_request.return_value = SAMPLE_CONFIG + mock_test_connection.return_value = ["ok", "My Music on myhost"] + config_data = config_entry.data + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=config_data + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My Music on myhost" + assert result["data"][CONF_HOST] == config_data[CONF_HOST] + assert result["data"][CONF_PORT] == config_data[CONF_PORT] + assert result["data"][CONF_PASSWORD] == config_data[CONF_PASSWORD] + + # Also test that creating a new entry with the same host aborts + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_updates_title(hass, config_entry): + """Test that zeroconf updates title and aborts with same host.""" + MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "different host"}).add_to_hass(hass) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + discovery_info = { + "host": "192.168.1.1", + "port": 23, + "properties": {"mtd-version": "27.0", "Machine Name": "zeroconf_test"}, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert config_entry.title == "zeroconf_test" + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + + +async def test_config_flow_no_websocket(hass, config_entry): + """Test config flow setup without websocket enabled on server.""" + with patch( + "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection" + ) as mock_test_connection: + # test invalid config data + mock_test_connection.return_value = ["websocket_not_enabled"] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_config_flow_zeroconf_invalid(hass): + """Test that an invalid zeroconf entry doesn't work.""" + # test with no discovery properties + discovery_info = {"host": "127.0.0.1", "port": 23} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) # doesn't create the entry, tries to show form but gets abort + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_forked_daapd" + # test with forked-daapd version < 27 + discovery_info = { + "host": "127.0.0.1", + "port": 23, + "properties": {"mtd-version": "26.3", "Machine Name": "forked-daapd"}, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) # doesn't create the entry, tries to show form but gets abort + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_forked_daapd" + # test with verbose mtd-version from Firefly + discovery_info = { + "host": "127.0.0.1", + "port": 23, + "properties": {"mtd-version": "0.2.4.1", "Machine Name": "firefly"}, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) # doesn't create the entry, tries to show form but gets abort + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_forked_daapd" + + +async def test_config_flow_zeroconf_valid(hass): + """Test that a valid zeroconf entry works.""" + discovery_info = { + "host": "192.168.1.1", + "port": 23, + "properties": { + "mtd-version": "27.0", + "Machine Name": "zeroconf_test", + "Machine ID": "5E55EEFF", + }, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_options_flow(hass, config_entry): + """Test config flow options.""" + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI.get_request", + autospec=True, + ) as mock_get_request: + mock_get_request.return_value = SAMPLE_CONFIG + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_TTS_PAUSE_TIME: 0.05, + CONF_TTS_VOLUME: 0.8, + CONF_LIBRESPOT_JAVA_PORT: 0, + CONF_MAX_PLAYLISTS: 8, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py new file mode 100644 index 00000000000..8f3a6c2c139 --- /dev/null +++ b/tests/components/forked_daapd/test_media_player.py @@ -0,0 +1,755 @@ +"""The media player tests for the forked_daapd media player platform.""" + +import pytest + +from homeassistant.components.forked_daapd.const import ( + CONF_LIBRESPOT_JAVA_PORT, + CONF_MAX_PLAYLISTS, + CONF_TTS_PAUSE_TIME, + CONF_TTS_VOLUME, + DOMAIN, + SOURCE_NAME_CLEAR, + SOURCE_NAME_DEFAULT, + SUPPORTED_FEATURES, + SUPPORTED_FEATURES_ZONE, +) +from homeassistant.components.media_player import ( + SERVICE_CLEAR_PLAYLIST, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, + SERVICE_MEDIA_STOP, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, + SERVICE_SHUFFLE_SET, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, +) +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_ALBUM_ARTIST, + ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, + ATTR_MEDIA_POSITION, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_TITLE, + ATTR_MEDIA_TRACK, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN as MP_DOMAIN, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, +) +from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_USER +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + STATE_ON, + STATE_PAUSED, + STATE_UNAVAILABLE, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" +TEST_ZONE_ENTITY_NAMES = [ + "media_player.forked_daapd_output_" + x + for x in ["kitchen", "computer", "daapd_fifo"] +] + +OPTIONS_DATA = { + CONF_LIBRESPOT_JAVA_PORT: "123", + CONF_MAX_PLAYLISTS: 8, + CONF_TTS_PAUSE_TIME: 0, + CONF_TTS_VOLUME: 0.25, +} + +SAMPLE_PLAYER_PAUSED = { + "state": "pause", + "repeat": "off", + "consume": False, + "shuffle": False, + "volume": 20, + "item_id": 12322, + "item_length_ms": 50, + "item_progress_ms": 5, +} + +SAMPLE_PLAYER_PLAYING = { + "state": "play", + "repeat": "off", + "consume": False, + "shuffle": False, + "volume": 50, + "item_id": 12322, + "item_length_ms": 50, + "item_progress_ms": 5, +} + +SAMPLE_PLAYER_STOPPED = { + "state": "stop", + "repeat": "off", + "consume": False, + "shuffle": False, + "volume": 0, + "item_id": 12322, + "item_length_ms": 50, + "item_progress_ms": 5, +} + +SAMPLE_QUEUE_TTS = { + "version": 833, + "count": 1, + "items": [ + { + "id": 12322, + "position": 0, + "track_id": 1234, + "title": "Short TTS file", + "artist": "Google", + "album": "No album", + "album_artist": "The xx", + "artwork_url": "http://art", + "length_ms": 0, + "track_number": 1, + "media_kind": "music", + "data_kind": "url", + "uri": "tts_proxy_somefile.mp3", + } + ], +} + +SAMPLE_QUEUE_PIPE = { + "version": 833, + "count": 1, + "items": [ + { + "id": 12322, + "title": "librespot-java", + "artist": "some artist", + "album": "some album", + "album_artist": "The xx", + "length_ms": 0, + "track_number": 1, + "media_kind": "music", + "data_kind": "pipe", + "uri": "pipeuri", + } + ], +} + +SAMPLE_CONFIG = { + "websocket_port": 3688, + "version": "25.0", + "buildoptions": [ + "ffmpeg", + "iTunes XML", + "Spotify", + "LastFM", + "MPD", + "Device verification", + "Websockets", + "ALSA", + ], +} + +SAMPLE_CONFIG_NO_WEBSOCKET = { + "websocket_port": 0, + "version": "25.0", + "buildoptions": [ + "ffmpeg", + "iTunes XML", + "Spotify", + "LastFM", + "MPD", + "Device verification", + "Websockets", + "ALSA", + ], +} + + +SAMPLE_OUTPUTS_ON = ( + { + "id": "123456789012345", + "name": "kitchen", + "type": "AirPlay", + "selected": True, + "has_password": False, + "requires_auth": False, + "needs_auth_key": False, + "volume": 50, + }, + { + "id": "0", + "name": "Computer", + "type": "ALSA", + "selected": True, + "has_password": False, + "requires_auth": False, + "needs_auth_key": False, + "volume": 19, + }, + { + "id": "100", + "name": "daapd-fifo", + "type": "fifo", + "selected": False, + "has_password": False, + "requires_auth": False, + "needs_auth_key": False, + "volume": 0, + }, +) + + +SAMPLE_OUTPUTS_UNSELECTED = [ + { + "id": "123456789012345", + "name": "kitchen", + "type": "AirPlay", + "selected": False, + "has_password": False, + "requires_auth": False, + "needs_auth_key": False, + "volume": 0, + }, + { + "id": "0", + "name": "Computer", + "type": "ALSA", + "selected": False, + "has_password": False, + "requires_auth": False, + "needs_auth_key": False, + "volume": 19, + }, + { + "id": "100", + "name": "daapd-fifo", + "type": "fifo", + "selected": False, + "has_password": False, + "requires_auth": False, + "needs_auth_key": False, + "volume": 0, + }, +] + +SAMPLE_PIPES = [ + { + "id": 1, + "title": "librespot-java", + "media_kind": "music", + "data_kind": "pipe", + "path": "/music/srv/input.pipe", + "uri": "library:track:1", + } +] + +SAMPLE_PLAYLISTS = [{"id": 7, "name": "test_playlist", "uri": "library:playlist:2"}] + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create hass config_entry fixture.""" + data = { + CONF_HOST: "192.168.1.1", + CONF_PORT: "2345", + CONF_PASSWORD: "", + } + return MockConfigEntry( + version=1, + domain=DOMAIN, + title="", + data=data, + options={CONF_TTS_PAUSE_TIME: 0}, + system_options={}, + source=SOURCE_USER, + connection_class=CONN_CLASS_LOCAL_PUSH, + entry_id=1, + ) + + +@pytest.fixture(name="get_request_return_values") +async def get_request_return_values_fixture(): + """Get request return values we can change later.""" + return { + "config": SAMPLE_CONFIG, + "outputs": SAMPLE_OUTPUTS_ON, + "player": SAMPLE_PLAYER_PAUSED, + "queue": SAMPLE_QUEUE_TTS, + } + + +@pytest.fixture(name="mock_api_object") +async def mock_api_object_fixture(hass, config_entry, get_request_return_values): + """Create mock api fixture.""" + + async def get_request_side_effect(update_type): + if update_type == "outputs": + return {"outputs": get_request_return_values["outputs"]} + return get_request_return_values[update_type] + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + mock_api.return_value.get_request.side_effect = get_request_side_effect + mock_api.return_value.full_url.return_value = "" + mock_api.return_value.get_pipes.return_value = SAMPLE_PIPES + mock_api.return_value.get_playlists.return_value = SAMPLE_PLAYLISTS + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + + mock_api.return_value.start_websocket_handler.assert_called_once() + mock_api.return_value.get_request.assert_called_once() + updater_update = mock_api.return_value.start_websocket_handler.call_args[0][2] + await updater_update(["player", "outputs", "queue"]) + await hass.async_block_till_done() + + async def add_to_queue_side_effect( + uris, playback=None, playback_from_position=None, clear=None + ): + await updater_update(["queue", "player"]) + + mock_api.return_value.add_to_queue.side_effect = ( + add_to_queue_side_effect # for play_media testing + ) + + async def pause_side_effect(): + await updater_update(["player"]) + + mock_api.return_value.pause_playback.side_effect = pause_side_effect + + return mock_api.return_value + + +async def test_unload_config_entry(hass, config_entry, mock_api_object): + """Test the player is removed when the config entry is unloaded.""" + assert hass.states.get(TEST_MASTER_ENTITY_NAME) + assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]) + await config_entry.async_unload(hass) + assert not hass.states.get(TEST_MASTER_ENTITY_NAME) + assert not hass.states.get(TEST_ZONE_ENTITY_NAMES[0]) + + +def test_master_state(hass, mock_api_object): + """Test master state attributes.""" + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == STATE_PAUSED + assert state.attributes[ATTR_FRIENDLY_NAME] == "forked-daapd server" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES + assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED] + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.2 + assert state.attributes[ATTR_MEDIA_CONTENT_ID] == 12322 + assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert state.attributes[ATTR_MEDIA_DURATION] == 0.05 + assert state.attributes[ATTR_MEDIA_POSITION] == 0.005 + assert state.attributes[ATTR_MEDIA_TITLE] == "Short TTS file" + assert state.attributes[ATTR_MEDIA_ARTIST] == "Google" + assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "No album" + assert state.attributes[ATTR_MEDIA_ALBUM_ARTIST] == "The xx" + assert state.attributes[ATTR_MEDIA_TRACK] == 1 + assert not state.attributes[ATTR_MEDIA_SHUFFLE] + + +async def _service_call( + hass, entity_name, service, additional_service_data=None, blocking=True +): + if additional_service_data is None: + additional_service_data = {} + return await hass.services.async_call( + MP_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: entity_name, **additional_service_data}, + blocking=blocking, + ) + + +async def test_zone(hass, mock_api_object): + """Test zone attributes and methods.""" + zone_entity_name = TEST_ZONE_ENTITY_NAMES[0] + state = hass.states.get(zone_entity_name) + assert state.attributes[ATTR_FRIENDLY_NAME] == "forked-daapd output (kitchen)" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES_ZONE + assert state.state == STATE_ON + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.5 + assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED] + await _service_call(hass, zone_entity_name, SERVICE_TURN_ON) + await _service_call(hass, zone_entity_name, SERVICE_TURN_OFF) + await _service_call(hass, zone_entity_name, SERVICE_TOGGLE) + await _service_call( + hass, zone_entity_name, SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0.3} + ) + await _service_call( + hass, zone_entity_name, SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + ) + await _service_call( + hass, zone_entity_name, SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False} + ) + zone_entity_name = TEST_ZONE_ENTITY_NAMES[2] + await _service_call(hass, zone_entity_name, SERVICE_TOGGLE) + await _service_call( + hass, zone_entity_name, SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + ) + output_id = SAMPLE_OUTPUTS_ON[0]["id"] + initial_volume = SAMPLE_OUTPUTS_ON[0]["volume"] + mock_api_object.change_output.assert_any_call(output_id, selected=True) + mock_api_object.change_output.assert_any_call(output_id, selected=False) + mock_api_object.set_volume.assert_any_call(output_id=output_id, volume=30) + mock_api_object.set_volume.assert_any_call(output_id=output_id, volume=0) + mock_api_object.set_volume.assert_any_call( + output_id=output_id, volume=initial_volume + ) + output_id = SAMPLE_OUTPUTS_ON[2]["id"] + mock_api_object.change_output.assert_any_call(output_id, selected=True) + + +async def test_last_outputs_master(hass, mock_api_object): + """Test restoration of _last_outputs.""" + # Test turning on sends API call + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_ON) + assert mock_api_object.change_output.call_count == 0 + assert mock_api_object.set_enabled_outputs.call_count == 1 + await _service_call( + hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_OFF + ) # should have stored last outputs + assert mock_api_object.change_output.call_count == 0 + assert mock_api_object.set_enabled_outputs.call_count == 2 + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_ON) + assert mock_api_object.change_output.call_count == 3 + assert mock_api_object.set_enabled_outputs.call_count == 2 + + +async def test_bunch_of_stuff_master(hass, mock_api_object, get_request_return_values): + """Run bunch of stuff.""" + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_ON) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_OFF) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TOGGLE) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: True}, + ) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: False}, + ) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 0.5}, + ) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_PAUSE) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_PLAY) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_STOP) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_PREVIOUS_TRACK) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_NEXT_TRACK) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_MEDIA_SEEK, + {ATTR_MEDIA_SEEK_POSITION: 35}, + ) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_CLEAR_PLAYLIST) + await _service_call( + hass, TEST_MASTER_ENTITY_NAME, SERVICE_SHUFFLE_SET, {ATTR_MEDIA_SHUFFLE: False} + ) + # stop player and run more stuff + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.2 + get_request_return_values["player"] = SAMPLE_PLAYER_STOPPED + updater_update = mock_api_object.start_websocket_handler.call_args[0][2] + await updater_update(["player"]) + await hass.async_block_till_done() + # mute from volume==0 + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0 + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: True}, + ) + # now turn off (stopped and all outputs unselected) + get_request_return_values["outputs"] = SAMPLE_OUTPUTS_UNSELECTED + await updater_update(["outputs"]) + await hass.async_block_till_done() + # toggle from off + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TOGGLE) + for output in SAMPLE_OUTPUTS_ON: + mock_api_object.change_output.assert_any_call( + output["id"], selected=output["selected"], volume=output["volume"], + ) + mock_api_object.set_volume.assert_any_call(volume=0) + mock_api_object.set_volume.assert_any_call(volume=SAMPLE_PLAYER_PAUSED["volume"]) + mock_api_object.set_volume.assert_any_call(volume=50) + mock_api_object.set_enabled_outputs.assert_any_call( + [output["id"] for output in SAMPLE_OUTPUTS_ON] + ) + mock_api_object.set_enabled_outputs.assert_any_call([]) + mock_api_object.start_playback.assert_called_once() + assert mock_api_object.pause_playback.call_count == 3 + mock_api_object.stop_playback.assert_called_once() + mock_api_object.previous_track.assert_called_once() + mock_api_object.next_track.assert_called_once() + mock_api_object.seek.assert_called_once() + mock_api_object.shuffle.assert_called_once() + mock_api_object.clear_queue.assert_called_once() + + +async def test_async_play_media_from_paused(hass, mock_api_object): + """Test async play media from paused.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + + +async def test_async_play_media_from_stopped( + hass, get_request_return_values, mock_api_object +): + """Test async play media from stopped.""" + updater_update = mock_api_object.start_websocket_handler.call_args[0][2] + + get_request_return_values["player"] = SAMPLE_PLAYER_STOPPED + await updater_update(["player"]) + await hass.async_block_till_done() + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + + +async def test_async_play_media_unsupported(hass, mock_api_object): + """Test async play media on unsupported media type.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_TVSHOW, + ATTR_MEDIA_CONTENT_ID: "wontwork.mp4", + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.last_updated == initial_state.last_updated + + +async def test_async_play_media_tts_timeout(hass, mock_api_object): + """Test async play media with TTS timeout.""" + mock_api_object.add_to_queue.side_effect = None + with patch("homeassistant.components.forked_daapd.media_player.TTS_TIMEOUT", 0): + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + + +async def test_use_pipe_control_with_no_api(hass, mock_api_object): + """Test using pipe control with no api set.""" + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_SELECT_SOURCE, + {ATTR_INPUT_SOURCE: "librespot-java (pipe)"}, + ) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_PLAY) + assert mock_api_object.start_playback.call_count == 0 + + +async def test_clear_source(hass, mock_api_object): + """Test changing source to clear.""" + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_SELECT_SOURCE, + {ATTR_INPUT_SOURCE: SOURCE_NAME_CLEAR}, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.attributes[ATTR_INPUT_SOURCE] == SOURCE_NAME_DEFAULT + + +@pytest.fixture(name="pipe_control_api_object") +async def pipe_control_api_object_fixture( + hass, config_entry, get_request_return_values, mock_api_object +): + """Fixture for mock librespot_java api.""" + with patch( + "homeassistant.components.forked_daapd.media_player.LibrespotJavaAPI", + autospec=True, + ) as pipe_control_api: + hass.config_entries.async_update_entry(config_entry, options=OPTIONS_DATA) + await hass.async_block_till_done() + get_request_return_values["player"] = SAMPLE_PLAYER_PLAYING + updater_update = mock_api_object.start_websocket_handler.call_args[0][2] + await updater_update(["player"]) + await hass.async_block_till_done() + + async def pause_side_effect(): + await updater_update(["player"]) + + pipe_control_api.return_value.player_pause.side_effect = pause_side_effect + + await updater_update(["database"]) # load in sources + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_SELECT_SOURCE, + {ATTR_INPUT_SOURCE: "librespot-java (pipe)"}, + ) + + return pipe_control_api.return_value + + +async def test_librespot_java_stuff( + hass, get_request_return_values, mock_api_object, pipe_control_api_object +): + """Test options update and librespot-java stuff.""" + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.attributes[ATTR_INPUT_SOURCE] == "librespot-java (pipe)" + # call some basic services + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_STOP) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_PREVIOUS_TRACK) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_NEXT_TRACK) + await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_MEDIA_PLAY) + pipe_control_api_object.player_pause.assert_called_once() + pipe_control_api_object.player_prev.assert_called_once() + pipe_control_api_object.player_next.assert_called_once() + pipe_control_api_object.player_resume.assert_called_once() + # switch away + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_SELECT_SOURCE, + {ATTR_INPUT_SOURCE: SOURCE_NAME_DEFAULT}, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.attributes[ATTR_INPUT_SOURCE] == SOURCE_NAME_DEFAULT + # test pipe getting queued externally changes source + get_request_return_values["queue"] = SAMPLE_QUEUE_PIPE + updater_update = mock_api_object.start_websocket_handler.call_args[0][2] + await updater_update(["queue"]) + await hass.async_block_till_done() + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.attributes[ATTR_INPUT_SOURCE] == "librespot-java (pipe)" + + +async def test_librespot_java_play_media(hass, pipe_control_api_object): + """Test play media with librespot-java pipe.""" + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + + +async def test_librespot_java_play_media_pause_timeout(hass, pipe_control_api_object): + """Test play media with librespot-java pipe.""" + # test media play with pause timeout + pipe_control_api_object.player_pause.side_effect = None + with patch( + "homeassistant.components.forked_daapd.media_player.CALLBACK_TIMEOUT", 0 + ): + initial_state = hass.states.get(TEST_MASTER_ENTITY_NAME) + await _service_call( + hass, + TEST_MASTER_ENTITY_NAME, + SERVICE_PLAY_MEDIA, + { + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + }, + ) + state = hass.states.get(TEST_MASTER_ENTITY_NAME) + assert state.state == initial_state.state + assert state.last_updated > initial_state.last_updated + + +async def test_unsupported_update(hass, mock_api_object): + """Test unsupported update type.""" + last_updated = hass.states.get(TEST_MASTER_ENTITY_NAME).last_updated + updater_update = mock_api_object.start_websocket_handler.call_args[0][2] + await updater_update(["config"]) + await hass.async_block_till_done() + assert hass.states.get(TEST_MASTER_ENTITY_NAME).last_updated == last_updated + + +async def test_invalid_websocket_port(hass, config_entry): + """Test invalid websocket port on async_init.""" + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + mock_api.return_value.get_request.return_value = SAMPLE_CONFIG_NO_WEBSOCKET + config_entry.add_to_hass(hass) + await config_entry.async_setup(hass) + await hass.async_block_till_done() + assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE + + +async def test_websocket_disconnect(hass, mock_api_object): + """Test websocket disconnection.""" + assert hass.states.get(TEST_MASTER_ENTITY_NAME).state != STATE_UNAVAILABLE + assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state != STATE_UNAVAILABLE + updater_disconnected = mock_api_object.start_websocket_handler.call_args[0][4] + updater_disconnected() + await hass.async_block_till_done() + assert hass.states.get(TEST_MASTER_ENTITY_NAME).state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ZONE_ENTITY_NAMES[0]).state == STATE_UNAVAILABLE diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index e813469cbbf..7581b03ce72 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -1,8 +1,8 @@ """Test helpers for Freebox.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def mock_path(): diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 68e787e1ba0..a4c8a950299 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -4,7 +4,6 @@ from aiofreepybox.exceptions import ( HttpRequestError, InvalidTokenError, ) -from asynctest import CoroutineMock, patch import pytest from homeassistant import data_entry_flow @@ -12,6 +11,7 @@ from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry HOST = "myrouter.freeboxos.fr" @@ -22,11 +22,17 @@ PORT = 1234 def mock_controller_connect(): """Mock a successful connection.""" with patch("homeassistant.components.freebox.router.Freepybox") as service_mock: - service_mock.return_value.open = CoroutineMock() - service_mock.return_value.system.get_config = CoroutineMock() - service_mock.return_value.lan.get_hosts_list = CoroutineMock() - service_mock.return_value.connection.get_status = CoroutineMock() - service_mock.return_value.close = CoroutineMock() + service_mock.return_value.open = AsyncMock() + service_mock.return_value.system.get_config = AsyncMock( + return_value={ + "mac": "abcd", + "model_info": {"pretty_name": "Pretty Model"}, + "firmware_version": "123", + } + ) + service_mock.return_value.lan.get_hosts_list = AsyncMock() + service_mock.return_value.connection.get_status = AsyncMock() + service_mock.return_value.close = AsyncMock() yield service_mock diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index f19e05b84df..066b9a30cb3 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock - from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import Mock + MOCK_CONFIG = { DOMAIN: { CONF_DEVICES: [ diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 591c1037525..7dcee138382 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -1,8 +1,8 @@ """Fixtures for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, patch - import pytest +from tests.async_mock import Mock, patch + @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 89c1dea1704..b3157a3be33 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,7 +1,6 @@ """Tests for AVM Fritz!Box binary sensor component.""" from datetime import timedelta from unittest import mock -from unittest.mock import Mock from requests.exceptions import HTTPError @@ -19,6 +18,7 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceBinarySensorMock +from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 627eae5da91..519e3afa31a 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta -from unittest.mock import Mock, call from requests.exceptions import HTTPError @@ -42,6 +41,7 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceClimateMock +from tests.async_mock import Mock, call from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 8bfd992347f..35b41d52118 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box config flow.""" from unittest import mock -from unittest.mock import Mock, patch from pyfritzhome import LoginError import pytest @@ -17,6 +16,8 @@ from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG +from tests.async_mock import Mock, patch + MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 11067c1aa51..55dab3626db 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,6 +1,4 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, call - from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED @@ -10,6 +8,7 @@ from homeassistant.setup import async_setup_component from . import MOCK_CONFIG, FritzDeviceSwitchMock +from tests.async_mock import Mock, call from tests.common import MockConfigEntry diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 6dde22f074e..7f97e8abfb1 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box sensor component.""" from datetime import timedelta -from unittest.mock import Mock from requests.exceptions import HTTPError @@ -21,6 +20,7 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceSensorMock +from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 1c0f7b3f37a..c9e05b2d481 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box switch component.""" from datetime import timedelta -from unittest.mock import Mock from requests.exceptions import HTTPError @@ -29,6 +28,7 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceSwitchMock +from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index ef254871830..10f55bd4db3 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,7 +1,6 @@ """The tests for Home Assistant frontend.""" import re -from asynctest import patch import pytest from homeassistant.components.frontend import ( @@ -17,6 +16,7 @@ from homeassistant.const import HTTP_NOT_FOUND from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_capture_events CONFIG_THEMES = {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}} diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 276b6f46871..1c383d4343a 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,6 +1,4 @@ """Test the Garmin Connect config flow.""" -from unittest.mock import patch - from garminconnect import ( GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -12,6 +10,7 @@ from homeassistant import data_entry_flow from homeassistant.components.garmin_connect.const import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONF = { diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py index 6e61b86dbb7..648ab08507b 100644 --- a/tests/components/gdacs/__init__.py +++ b/tests/components/gdacs/__init__.py @@ -1,5 +1,5 @@ """Tests for the GDACS component.""" -from unittest.mock import MagicMock +from tests.async_mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index c3c5f5609c4..10e4312eb38 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -1,7 +1,6 @@ """Define tests for the GDACS config flow.""" from datetime import timedelta -from asynctest import patch import pytest from homeassistant import data_entry_flow @@ -13,6 +12,8 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) +from tests.async_mock import patch + @pytest.fixture(name="gdacs_setup", autouse=True) def gdacs_setup_fixture(): diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 3b49fe2af71..2162340154f 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -1,8 +1,6 @@ """The tests for the GDACS Feed integration.""" import datetime -from asynctest import patch - from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED from homeassistant.components.gdacs.geo_location import ( @@ -35,6 +33,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py index 40bda2a196b..c0ac83ebcc2 100644 --- a/tests/components/gdacs/test_init.py +++ b/tests/components/gdacs/test_init.py @@ -1,8 +1,8 @@ """Define tests for the GDACS general setup.""" -from asynctest import patch - from homeassistant.components.gdacs import DOMAIN, FEED +from tests.async_mock import patch + async def test_component_unload_config_entry(hass, config_entry): """Test that loading and unloading of a config entry works.""" diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index 5e8fd5ad30f..aa8c2a43428 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the GDACS Feed integration.""" -from asynctest import patch - from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL from homeassistant.components.gdacs.sensor import ( @@ -20,6 +18,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 264146a6fda..f0b29539ed5 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,7 +1,6 @@ """The tests for the generic_thermostat.""" import datetime -from asynctest import mock import pytest import pytz import voluptuous as vol @@ -32,6 +31,7 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.async_mock import patch from tests.common import assert_setup_component, mock_restore_cache from tests.components.climate import common @@ -622,7 +622,7 @@ async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, False) @@ -650,7 +650,7 @@ async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, True) @@ -733,7 +733,7 @@ async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, False) @@ -761,7 +761,7 @@ async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, True) @@ -852,7 +852,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, False) @@ -871,7 +871,7 @@ async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, True) diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index e10125f84ac..d79fa3cb18d 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,6 +1,4 @@ """The tests for the geojson platform.""" -from asynctest.mock import MagicMock, call, patch - from homeassistant.components import geo_location from homeassistant.components.geo_json_events.geo_location import ( ATTR_EXTERNAL_ID, @@ -23,6 +21,7 @@ from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed URL = "http://geo.json.local/geo_json_events.json" @@ -189,8 +188,8 @@ async def test_setup_race_condition(hass): # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 15.5, (-31.0, 150.0)) - delete_signal = f"geo_json_events_delete_1234" - update_signal = f"geo_json_events_update_1234" + delete_signal = "geo_json_events_delete_1234" + update_signal = "geo_json_events_update_1234" # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 25243afea78..9f7cdd3faab 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -1,7 +1,6 @@ """The test for the geo rss events sensor platform.""" import unittest from unittest import mock -from unittest.mock import MagicMock, patch from homeassistant.components import sensor import homeassistant.components.geo_rss_events.sensor as geo_rss_events @@ -14,6 +13,7 @@ from homeassistant.const import ( from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, patch from tests.common import ( assert_setup_component, fire_time_changed, diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index b988d613d6c..3a9bdf1fe73 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,12 +1,10 @@ """The tests for the Geofency device tracker platform.""" -# pylint: disable=redefined-outer-name -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components import zone from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, @@ -16,6 +14,9 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util import slugify +# pylint: disable=redefined-outer-name +from tests.async_mock import patch + HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -148,7 +149,9 @@ async def setup_zones(loop, hass): @pytest.fixture async def webhook_id(hass, geofency_client): """Initialize the Geofency component and get the webhook_id.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py index 424c6372ea8..82cb62b3939 100644 --- a/tests/components/geonetnz_quakes/__init__.py +++ b/tests/components/geonetnz_quakes/__init__.py @@ -1,5 +1,5 @@ """Tests for the geonetnz_quakes component.""" -from unittest.mock import MagicMock +from tests.async_mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 0264baa8e87..89644445ef1 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,8 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch - from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED @@ -31,6 +29,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_quakes/test_init.py b/tests/components/geonetnz_quakes/test_init.py index 85724879f7b..87f2f2a7947 100644 --- a/tests/components/geonetnz_quakes/test_init.py +++ b/tests/components/geonetnz_quakes/test_init.py @@ -1,8 +1,8 @@ """Define tests for the GeoNet NZ Quakes general setup.""" -from asynctest import patch - from homeassistant.components.geonetnz_quakes import DOMAIN, FEED +from tests.async_mock import patch + async def test_component_unload_config_entry(hass, config_entry): """Test that loading and unloading of a config entry works.""" diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 7d7f8333bc0..f02a803994e 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,8 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch - from homeassistant.components import geonetnz_quakes from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL from homeassistant.components.geonetnz_quakes.sensor import ( @@ -22,6 +20,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py index 708b69e0031..023cab46ec8 100644 --- a/tests/components/geonetnz_volcano/__init__.py +++ b/tests/components/geonetnz_volcano/__init__.py @@ -1,5 +1,5 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" -from unittest.mock import MagicMock +from tests.async_mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py index 3e2566ffb81..4edf8f452fe 100644 --- a/tests/components/geonetnz_volcano/test_init.py +++ b/tests/components/geonetnz_volcano/test_init.py @@ -1,15 +1,15 @@ """Define tests for the GeoNet NZ Volcano general setup.""" -from asynctest import CoroutineMock, patch - from homeassistant.components.geonetnz_volcano import DOMAIN, FEED +from tests.async_mock import AsyncMock, patch + async def test_component_unload_config_entry(hass, config_entry): """Test that loading and unloading of a config entry works.""" config_entry.add_to_hass(hass) with patch( "aio_geojson_geonetnz_volcano.GeonetnzVolcanoFeedManager.update", - new_callable=CoroutineMock, + new_callable=AsyncMock, ) as mock_feed_manager_update: # Load config entry. assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index 8f71e3c4757..ccf6248bfa9 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" -from asynctest import CoroutineMock, patch - from homeassistant.components import geonetnz_volcano from homeassistant.components.geo_location import ATTR_DISTANCE from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL @@ -23,6 +21,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed from tests.components.geonetnz_volcano import _generate_mock_feed_entry @@ -49,7 +48,7 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock ) as mock_feed_update: mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) @@ -139,7 +138,7 @@ async def test_setup_imperial(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock ) as mock_feed_update, patch( "aio_geojson_client.feed.GeoJsonFeed.__init__" ) as mock_feed_init: diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 3a4aff6d9ad..b2f7ceec9e4 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the GIOS config flow.""" -from asynctest import patch from gios import ApiError from homeassistant import data_entry_flow @@ -7,6 +6,8 @@ from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME +from tests.async_mock import patch + CONFIG = { CONF_NAME: "Foo", CONF_STATION_ID: 123, diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 20cb13130ec..6ec75ad53f6 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,8 +1,8 @@ """Test configuration and mocks for the google integration.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + TEST_CALENDAR = { "id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com", "etag": '"3584134138943410"', diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index ad7b6b12001..92f03396965 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1,6 +1,5 @@ """The tests for the google calendar platform.""" import copy -from unittest.mock import Mock, patch import httplib2 import pytest @@ -23,6 +22,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import async_mock_service GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 59a9f9f5ab2..6f7ce74ce62 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,11 +1,11 @@ """The tests for the Google Calendar component.""" -from unittest.mock import patch - import pytest import homeassistant.components.google as google from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(name="google_setup") def mock_google_setup(hass): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 79684bdeb44..8f6b0908f83 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,8 +1,8 @@ """Tests for the Google Assistant integration.""" -from asynctest.mock import MagicMock - from homeassistant.components.google_assistant import helpers +from tests.async_mock import MagicMock + def mock_google_config_store(agent_user_ids=None): """Fake a storage for google assistant.""" diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 9f6f83fab59..05afd29a5bd 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,7 +1,6 @@ """Test Google Assistant helpers.""" from datetime import timedelta -from asynctest.mock import Mock, call, patch import pytest from homeassistant.components.google_assistant import helpers @@ -9,11 +8,13 @@ from homeassistant.components.google_assistant.const import ( # noqa: F401 EVENT_COMMAND_RECEIVED, NOT_EXPOSE_LOCAL, ) +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component from homeassistant.util import dt from . import MockConfig +from tests.async_mock import Mock, call, patch from tests.common import ( async_capture_events, async_fire_time_changed, @@ -24,7 +25,11 @@ from tests.common import ( async def test_google_entity_sync_serialize_with_local_sdk(hass): """Test sync serialize attributes of a GoogleEntity.""" hass.states.async_set("light.ceiling_lights", "off") - hass.config.api = Mock(port=1234, use_ssl=True, base_url="https://hostname:1234") + hass.config.api = Mock(port=1234, use_ssl=True) + await async_process_ha_core_config( + hass, {"external_url": "https://hostname:1234"}, + ) + hass.http = Mock(server_port=1234) config = MockConfig( hass=hass, diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index ff159e4e10c..4b9461e6304 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,8 +1,6 @@ """Test Google http services.""" from datetime import datetime, timedelta, timezone -from asynctest import ANY, patch - from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA from homeassistant.components.google_assistant.const import ( HOMEGRAPH_TOKEN_URL, @@ -14,6 +12,8 @@ from homeassistant.components.google_assistant.http import ( _get_homegraph_token, ) +from tests.async_mock import ANY, patch + DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { "project_id": "1234", diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index fd9cad27ffa..7dd3faba7ab 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,12 +1,11 @@ """Test Google report state.""" -from unittest.mock import patch - from homeassistant.components.google_assistant import error, report_state from homeassistant.util.dt import utcnow from . import BASIC_CONFIG -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import AsyncMock, patch +from tests.common import async_fire_time_changed async def test_report_state(hass, caplog): @@ -15,7 +14,7 @@ async def test_report_state(hass, caplog): hass.states.async_set("switch.ac", "on") with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) @@ -34,7 +33,7 @@ async def test_report_state(hass, caplog): } with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() @@ -47,7 +46,7 @@ async def test_report_state(hass, caplog): # Test that state changes that change something that Google doesn't care about # do not trigger a state report. with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set( "light.kitchen", "on", {"irrelevant": "should_be_ignored"} @@ -58,7 +57,7 @@ async def test_report_state(hass, caplog): # Test that entities that we can't query don't report a state with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report, patch( "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", side_effect=error.SmartHomeError("mock-error", "mock-msg"), @@ -72,7 +71,7 @@ async def test_report_state(hass, caplog): unsub() with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 42002d62906..e619356717f 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,6 +1,4 @@ """Test Google Smart Home.""" -from unittest.mock import Mock, patch - import pytest from homeassistant.components import camera @@ -22,6 +20,7 @@ from homeassistant.components.google_assistant import ( smart_home as sh, trait, ) +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ from homeassistant.core import EVENT_CALL_SERVICE, State from homeassistant.helpers import device_registry @@ -29,12 +28,8 @@ from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig -from tests.common import ( - mock_area_registry, - mock_coro, - mock_device_registry, - mock_registry, -) +from tests.async_mock import patch +from tests.common import mock_area_registry, mock_device_registry, mock_registry REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -262,6 +257,8 @@ async def test_query_message(hass): }, } + await hass.async_block_till_done() + assert len(events) == 4 assert events[0].event_type == EVENT_QUERY_RECEIVED assert events[0].data == { @@ -788,9 +785,7 @@ async def test_query_disconnect(hass): config = MockConfig(hass=hass) config.async_enable_report_state() assert config._unsub_report_state is not None - with patch.object( - config, "async_disconnect_agent_user", side_effect=mock_coro - ) as mock_disconnect: + with patch.object(config, "async_disconnect_agent_user") as mock_disconnect: result = await sh.async_handle_message( hass, config, @@ -804,14 +799,16 @@ async def test_query_disconnect(hass): async def test_trait_execute_adding_query_data(hass): """Test a trait execute influencing query data.""" - hass.config.api = Mock(base_url="http://1.1.1.1:8123") + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) hass.states.async_set( "camera.office", "idle", {"supported_features": camera.SUPPORT_STREAM} ) with patch( "homeassistant.components.camera.async_request_stream", - return_value=mock_coro("/api/streams/bla"), + return_value="/api/streams/bla", ): result = await sh.async_handle_message( hass, @@ -858,7 +855,7 @@ async def test_trait_execute_adding_query_data(hass): "status": "SUCCESS", "states": { "online": True, - "cameraStreamAccessUrl": "http://1.1.1.1:8123/api/streams/bla", + "cameraStreamAccessUrl": "https://example.com/api/streams/bla", }, } ] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a2b8f2e9ea7..801f4c1b5ba 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,6 +1,5 @@ """Tests for the Google Assistant traits.""" import logging -from unittest.mock import Mock, patch import pytest @@ -23,6 +22,7 @@ from homeassistant.components import ( ) from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import const, error, helpers, trait +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_DEVICE_CLASS, @@ -45,7 +45,8 @@ from homeassistant.util import color from . import BASIC_CONFIG, MockConfig -from tests.common import async_mock_service, mock_coro +from tests.async_mock import patch +from tests.common import async_mock_service _LOGGER = logging.getLogger(__name__) @@ -99,7 +100,9 @@ async def test_brightness_light(hass): async def test_camera_stream(hass): """Test camera stream trait support for camera domain.""" - hass.config.api = Mock(base_url="http://1.1.1.1:8123") + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) assert helpers.get_google_type(camera.DOMAIN, None) is not None assert trait.CameraStreamTrait.supported(camera.DOMAIN, camera.SUPPORT_STREAM, None) @@ -117,12 +120,12 @@ async def test_camera_stream(hass): with patch( "homeassistant.components.camera.async_request_stream", - return_value=mock_coro("/api/streams/bla"), + return_value="/api/streams/bla", ): await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}, {}) assert trt.query_attributes() == { - "cameraStreamAccessUrl": "http://1.1.1.1:8123/api/streams/bla" + "cameraStreamAccessUrl": "https://example.com/api/streams/bla" } diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 6703852528c..8e9ec9b7e1c 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -2,7 +2,6 @@ import asyncio import os import shutil -from unittest.mock import patch from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -10,8 +9,10 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) import homeassistant.components.tts as tts +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 @@ -23,6 +24,13 @@ class TestTTSGooglePlatform: """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + asyncio.run_coroutine_threadsafe( + async_process_ha_core_config( + self.hass, {"internal_url": "http://example.local:8123"} + ), + self.hass.loop, + ) + self.url = "https://translate.google.com/translate_tts" self.url_param = { "tl": "en", diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index 22059706dc5..69db8de184b 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Google Wifi platform.""" from datetime import datetime, timedelta import unittest -from unittest.mock import Mock, patch import requests_mock @@ -11,6 +10,7 @@ from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant NAME = "foo" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 9135f583d19..18e80647fe7 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,12 +1,11 @@ """The tests the for GPSLogger device tracker platform.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components import gpslogger, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, @@ -16,6 +15,8 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component +from tests.async_mock import patch + HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -62,7 +63,9 @@ async def setup_zones(loop, hass): @pytest.fixture async def webhook_id(hass, gpslogger_client): """Initialize the GPSLogger component and get the webhook_id.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index 88be3723936..19b8c165f37 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -2,7 +2,6 @@ import socket import unittest from unittest import mock -from unittest.mock import patch import homeassistant.components.graphite as graphite from homeassistant.const import ( @@ -15,6 +14,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/griddy/test_config_flow.py b/tests/components/griddy/test_config_flow.py index 1ab29aebece..79f99d7f8b1 100644 --- a/tests/components/griddy/test_config_flow.py +++ b/tests/components/griddy/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Griddy Power config flow.""" import asyncio -from asynctest import MagicMock, patch - from homeassistant import config_entries, setup from homeassistant.components.griddy.const import DOMAIN +from tests.async_mock import MagicMock, patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/griddy/test_sensor.py b/tests/components/griddy/test_sensor.py index 995327a9b56..ae3d0c3be84 100644 --- a/tests/components/griddy/test_sensor.py +++ b/tests/components/griddy/test_sensor.py @@ -2,12 +2,12 @@ import json import os -from asynctest import patch from griddypower.async_api import GriddyPriceData from homeassistant.components.griddy import CONF_LOADZONE, DOMAIN from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import load_fixture diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 467bd1ede95..c4d98ad37cc 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from collections import OrderedDict import unittest -from unittest.mock import patch import homeassistant.components.group as group from homeassistant.const import ( @@ -17,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant from tests.components.group import common diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 94d78d62877..70dab4472ed 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,8 +1,4 @@ """The tests for the Group Light platform.""" -from unittest.mock import MagicMock - -import asynctest - from homeassistant.components.group import DOMAIN import homeassistant.components.group.light as group from homeassistant.components.light import ( @@ -32,6 +28,9 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +import tests.async_mock +from tests.async_mock import MagicMock + async def test_default_state(hass): """Test light group default state.""" @@ -559,7 +558,7 @@ async def test_invalid_service_calls(hass): grouped_light = add_entities.call_args[0][0][0] grouped_light.hass = hass - with asynctest.patch.object(hass.services, "async_call") as mock_call: + with tests.async_mock.patch.object(hass.services, "async_call") as mock_call: await grouped_light.async_turn_on(brightness=150, four_oh_four="404") data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} mock_call.assert_called_once_with( diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index f029ec9d2fa..0925b318c9e 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,13 +1,13 @@ """The tests for the notify.group platform.""" import asyncio import unittest -from unittest.mock import MagicMock, patch import homeassistant.components.demo.notify as demo import homeassistant.components.group.notify as group import homeassistant.components.notify as notify from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 58bbc94876e..13422cd0826 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -1,11 +1,12 @@ """The tests for reproduction of state.""" from asyncio import Future -from unittest.mock import patch from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State +from tests.async_mock import patch + async def test_reproduce_group(hass): """Test reproduce_state with group.""" diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 93f909d3bd4..9cdb5799951 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Google Hangouts config flow.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from tests.async_mock import patch + EMAIL = "test@test.com" PASSWORD = "1232456" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 30421756d22..079923330e2 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,15 +1,15 @@ """Test the Logitech Harmony Hub config flow.""" -from asynctest import CoroutineMock, MagicMock, patch - from homeassistant import config_entries, setup from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN +from tests.async_mock import AsyncMock, MagicMock, patch + def _get_mock_harmonyapi(connect=None, close=None): harmonyapi_mock = MagicMock() - type(harmonyapi_mock).connect = CoroutineMock(return_value=connect) - type(harmonyapi_mock).close = CoroutineMock(return_value=close) + type(harmonyapi_mock).connect = AsyncMock(return_value=connect) + type(harmonyapi_mock).close = AsyncMock(return_value=close) return harmonyapi_mock diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 9a50da4ce41..5768d192c8a 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,6 +1,5 @@ """Fixtures for Hass.io.""" import os -from unittest.mock import Mock, patch import pytest @@ -10,7 +9,7 @@ from homeassistant.setup import async_setup_component from . import HASSIO_TOKEN -from tests.common import mock_coro +from tests.async_mock import Mock, patch @pytest.fixture @@ -18,9 +17,9 @@ def hassio_env(): """Fixture to inject hassio env.""" with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", - Mock(return_value=mock_coro({"result": "ok", "data": {}})), + return_value={"result": "ok", "data": {}}, ), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}), patch( - "homeassistant.components.hassio.HassIO.get_homeassistant_info", + "homeassistant.components.hassio.HassIO.get_info", Mock(side_effect=HassioAPIError()), ): yield @@ -31,13 +30,12 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): """Create mock hassio http client.""" with patch( "homeassistant.components.hassio.HassIO.update_hass_api", - return_value=mock_coro({"result": "ok"}), + return_value={"result": "ok"}, ) as hass_api, patch( "homeassistant.components.hassio.HassIO.update_hass_timezone", - return_value=mock_coro({"result": "ok"}), + return_value={"result": "ok"}, ), patch( - "homeassistant.components.hassio.HassIO.get_homeassistant_info", - side_effect=HassioAPIError(), + "homeassistant.components.hassio.HassIO.get_info", side_effect=HassioAPIError(), ): hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index d2ad673111d..9b11fd6dfc2 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,11 +1,9 @@ """Test add-on panel.""" -from unittest.mock import Mock, patch - import pytest from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch @pytest.fixture(autouse=True) @@ -49,7 +47,6 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env): with patch( "homeassistant.components.hassio.addon_panel._register_panel", - Mock(return_value=mock_coro()), ) as mock_panel: await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -92,7 +89,6 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli with patch( "homeassistant.components.hassio.addon_panel._register_panel", - Mock(return_value=mock_coro()), ) as mock_panel: await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 621efa1cb9e..e97c5bc66fb 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,10 +1,9 @@ """The tests for the hassio component.""" -from unittest.mock import Mock, patch from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import HomeAssistantError -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_auth_success(hass, hassio_client_supervisor): @@ -12,7 +11,6 @@ async def test_auth_success(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client_supervisor.post( "/api/hassio_auth", @@ -29,7 +27,6 @@ async def test_auth_fails_no_supervisor(hass, hassio_client): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client.post( "/api/hassio_auth", @@ -46,7 +43,6 @@ async def test_auth_fails_no_auth(hass, hassio_noauth_client): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_noauth_client.post( "/api/hassio_auth", @@ -110,7 +106,6 @@ async def test_login_success_extra(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client_supervisor.post( "/api/hassio_auth", @@ -131,7 +126,6 @@ async def test_password_success(hass, hassio_client_supervisor): """Test no auth needed for .""" with patch( "homeassistant.components.hassio.auth.HassIOPasswordReset._change_password", - Mock(return_value=mock_coro()), ) as mock_change: resp = await hassio_client_supervisor.post( "/api/hassio_auth/password_reset", @@ -147,7 +141,6 @@ async def test_password_fails_no_supervisor(hass, hassio_client): """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant.Data.async_save", - Mock(return_value=mock_coro()), ) as mock_save: resp = await hassio_client.post( "/api/hassio_auth/password_reset", @@ -163,7 +156,6 @@ async def test_password_fails_no_auth(hass, hassio_noauth_client): """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant.Data.async_save", - Mock(return_value=mock_coro()), ) as mock_save: resp = await hassio_noauth_client.post( "/api/hassio_auth/password_reset", @@ -179,7 +171,6 @@ async def test_password_no_user(hass, hassio_client_supervisor): """Test no auth needed for .""" with patch( "homeassistant.auth.providers.homeassistant.Data.async_save", - Mock(return_value=mock_coro()), ) as mock_save: resp = await hassio_client_supervisor.post( "/api/hassio_auth/password_reset", diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index a0d64440041..845c60c2f85 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,11 +1,9 @@ """Test config flow.""" -from unittest.mock import Mock, patch - from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): @@ -41,7 +39,7 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): with patch( "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - Mock(return_value=mock_coro({"type": "abort"})), + return_value={"type": "abort"}, ) as mock_mqtt: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -91,13 +89,13 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client with patch( "homeassistant.components.hassio.HassIO.update_hass_api", - Mock(return_value=mock_coro({"result": "ok"})), + return_value={"result": "ok"}, ), patch( - "homeassistant.components.hassio.HassIO.get_homeassistant_info", + "homeassistant.components.hassio.HassIO.get_info", Mock(side_effect=HassioAPIError()), ), patch( "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - Mock(return_value=mock_coro({"type": "abort"})), + return_value={"type": "abort"}, ) as mock_mqtt: await hass.async_start() await async_setup_component(hass, "hassio", {}) @@ -144,7 +142,7 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): with patch( "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - Mock(return_value=mock_coro({"type": "abort"})), + return_value={"type": "abort"}, ) as mock_mqtt: resp = await hassio_client.post( "/api/hassio_push/discovery/testuuid", diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 0b1bbd5237c..67fcfb75d5f 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -30,26 +30,64 @@ async def test_api_ping_exeption(hassio_handler, aioclient_mock): assert aioclient_mock.call_count == 1 -async def test_api_homeassistant_info(hassio_handler, aioclient_mock): - """Test setup with API Home Assistant info.""" +async def test_api_info(hassio_handler, aioclient_mock): + """Test setup with API generic info.""" aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", - json={"result": "ok", "data": {"last_version": "10.0"}}, + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, ) - data = await hassio_handler.get_homeassistant_info() + data = await hassio_handler.get_info() assert aioclient_mock.call_count == 1 - assert data["last_version"] == "10.0" + assert data["hassos"] is None + assert data["homeassistant"] == "0.110.0" + assert data["supervisor"] == "222" -async def test_api_homeassistant_info_error(hassio_handler, aioclient_mock): +async def test_api_info_error(hassio_handler, aioclient_mock): """Test setup with API Home Assistant info error.""" aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", json={"result": "error", "message": None} + "http://127.0.0.1/info", json={"result": "error", "message": None} ) with pytest.raises(HassioAPIError): - await hassio_handler.get_homeassistant_info() + await hassio_handler.get_info() + + assert aioclient_mock.call_count == 1 + + +async def test_api_host_info(hassio_handler, aioclient_mock): + """Test setup with API Host info.""" + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + ) + + data = await hassio_handler.get_host_info() + assert aioclient_mock.call_count == 1 + assert data["chassis"] == "vm" + assert data["kernel"] == "4.19.0-6-amd64" + assert data["operating_system"] == "Debian GNU/Linux 10 (buster)" + + +async def test_api_host_info_error(hassio_handler, aioclient_mock): + """Test setup with API Home Assistant info error.""" + aioclient_mock.get( + "http://127.0.0.1/host/info", json={"result": "error", "message": None} + ) + + with pytest.raises(HassioAPIError): + await hassio_handler.get_host_info() assert aioclient_mock.call_count == 1 diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index a5af24eb868..7386cf57d0c 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,9 +1,10 @@ """The tests for the hassio component.""" import asyncio -from unittest.mock import patch import pytest +from tests.async_mock import patch + async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 2751062dedf..c3110b6599c 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,6 +1,5 @@ """The tests for the hassio component.""" import os -from unittest.mock import Mock, patch import pytest @@ -9,7 +8,7 @@ from homeassistant.components import frontend from homeassistant.components.hassio import STORAGE_KEY from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} @@ -21,8 +20,25 @@ def mock_all(aioclient_mock): aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) aioclient_mock.get( - "http://127.0.0.1/homeassistant/info", - json={"result": "ok", "data": {"last_version": "10.0"}}, + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} @@ -35,8 +51,8 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 5 - assert hass.components.hassio.get_homeassistant_version() == "10.0" + assert aioclient_mock.call_count == 6 + assert hass.components.hassio.get_homeassistant_version() == "0.110.0" assert hass.components.hassio.is_hassio() @@ -74,7 +90,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -90,7 +106,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -102,7 +118,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -149,7 +165,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -163,7 +179,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" await hass.config.async_update(time_zone="America/New_York") @@ -179,7 +195,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" @@ -193,8 +209,7 @@ async def test_fail_setup_without_environ_var(hass): async def test_warn_when_cannot_connect(hass, caplog): """Fail warn when we cannot connect.""" with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.is_connected", - Mock(return_value=mock_coro(None)), + "homeassistant.components.hassio.HassIO.is_connected", return_value=None, ): result = await async_setup_component(hass, "hassio", {}) assert result @@ -311,7 +326,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): assert aioclient_mock.call_count == 4 with patch( - "homeassistant.config.async_check_ha_config_file", return_value=mock_coro() + "homeassistant.config.async_check_ha_config_file", return_value=None ) as mock_check_config: await hass.services.async_call("homeassistant", "restart") await hass.async_block_till_done() diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index cbaed220f11..4583079829a 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the hddtemp platform.""" import socket import unittest -from unittest.mock import patch from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 5201b7f7b8a..86be36e8188 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -1,7 +1,6 @@ """Configuration for HEOS tests.""" from typing import Dict, Sequence -from asynctest.mock import Mock, patch as patch from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest @@ -9,6 +8,7 @@ from homeassistant.components import ssdp from homeassistant.components.heos import DOMAIN from homeassistant.const import CONF_HOST +from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index b83923943bd..d90c4263240 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,7 +1,6 @@ """Tests for the Heos config flow module.""" from urllib.parse import urlparse -from asynctest import patch from pyheos import HeosError from homeassistant import data_entry_flow @@ -10,6 +9,8 @@ from homeassistant.components.heos.config_flow import HeosFlowHandler from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS from homeassistant.const import CONF_HOST +from tests.async_mock import patch + async def test_flow_aborts_already_setup(hass, config_entry): """Test flow aborts when entry already setup.""" diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index cfbdcb9198a..a6852e3db41 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,7 +1,6 @@ """Tests for the init module.""" import asyncio -from asynctest import Mock, patch from pyheos import CommandFailedError, HeosError, const import pytest @@ -20,6 +19,8 @@ from homeassistant.const import CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch + async def test_async_setup_creates_entry(hass, config): """Test component setup creates entry from config.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index d399f5b67aa..fd2922e94fe 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,6 +1,5 @@ """The test for the here_travel_time sensor platform.""" import logging -from unittest.mock import patch import urllib import herepy @@ -43,6 +42,7 @@ from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture DOMAIN = "sensor" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index 638fbe8f943..498e8c4c306 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -1,24 +1,22 @@ """Tests for the Hisense AEH-W4A1 init file.""" -from unittest.mock import patch - from pyaehw4a1 import exceptions from homeassistant import config_entries, data_entry_flow from homeassistant.components import hisense_aehw4a1 from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch async def test_creating_entry_sets_up_climate_discovery(hass): """Test setting up Hisense AEH-W4A1 loads the climate component.""" with patch( "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.discovery", - return_value=mock_coro(["1.2.3.4"]), + return_value=["1.2.3.4"], ): with patch( "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry", - return_value=mock_coro(True), + return_value=True, ) as mock_setup: result = await hass.config_entries.flow.async_init( hisense_aehw4a1.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -41,11 +39,11 @@ async def test_configuring_hisense_w4a1_create_entry(hass): """Test that specifying config will create an entry.""" with patch( "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", - return_value=mock_coro(True), + return_value=True, ): with patch( "homeassistant.components.hisense_aehw4a1.async_setup_entry", - return_value=mock_coro(True), + return_value=True, ) as mock_setup: await async_setup_component( hass, @@ -65,7 +63,7 @@ async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found(h ): with patch( "homeassistant.components.hisense_aehw4a1.async_setup_entry", - return_value=mock_coro(True), + return_value=True, ) as mock_setup: await async_setup_component( hass, @@ -80,8 +78,7 @@ async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found(h async def test_configuring_hisense_w4a1_not_creates_entry_for_empty_import(hass): """Test that specifying config will not create an entry.""" with patch( - "homeassistant.components.hisense_aehw4a1.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.hisense_aehw4a1.async_setup_entry", return_value=True, ) as mock_setup: await async_setup_component(hass, hisense_aehw4a1.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index b2687b2bd50..29e43c8428e 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -2,13 +2,13 @@ # pylint: disable=protected-access,invalid-name from datetime import timedelta import unittest -from unittest.mock import patch, sentinel from homeassistant.components import history, recorder import homeassistant.core as ha from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch, sentinel from tests.common import ( get_test_home_assistant, init_recorder_component, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 588e0df81db..d9f489d20b4 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from datetime import datetime, timedelta import unittest -from unittest.mock import patch import pytest import pytz @@ -14,6 +13,7 @@ from homeassistant.helpers.template import Template from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/home_connect/__init__.py b/tests/components/home_connect/__init__.py new file mode 100644 index 00000000000..2b61501c59a --- /dev/null +++ b/tests/components/home_connect/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Connect integration.""" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py new file mode 100644 index 00000000000..be6c21fe0a7 --- /dev/null +++ b/tests/components/home_connect/test_config_flow.py @@ -0,0 +1,54 @@ +"""Test the Home Connect config flow.""" +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.home_connect.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.helpers import config_entry_oauth2_flow + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "home_connect", + { + "home_connect": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "home_connect", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 38a76b7c3fb..b9309d70d63 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access import asyncio import unittest -from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -33,11 +32,11 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import ( async_capture_events, async_mock_service, get_test_home_assistant, - mock_coro, mock_service, patch_yaml_files, ) @@ -215,15 +214,15 @@ class TestComponentsCore(unittest.TestCase): assert mock_error.called assert mock_process.called is False - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=mock_coro()) + @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) def test_stop_homeassistant(self, mock_stop): """Test stop service.""" stop(self.hass) self.hass.block_till_done() assert mock_stop.called - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=mock_coro()) - @patch("homeassistant.config.async_check_ha_config_file", return_value=mock_coro()) + @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) + @patch("homeassistant.config.async_check_ha_config_file", return_value=None) def test_restart_homeassistant(self, mock_check, mock_restart): """Test stop service.""" restart(self.hass) @@ -231,7 +230,7 @@ class TestComponentsCore(unittest.TestCase): assert mock_restart.called assert mock_check.called - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=mock_coro()) + @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) @patch( "homeassistant.config.async_check_ha_config_file", side_effect=HomeAssistantError("Test error"), @@ -243,8 +242,8 @@ class TestComponentsCore(unittest.TestCase): assert mock_check.called assert not mock_restart.called - @patch("homeassistant.core.HomeAssistant.async_stop", return_value=mock_coro()) - @patch("homeassistant.config.async_check_ha_config_file", return_value=mock_coro()) + @patch("homeassistant.core.HomeAssistant.async_stop", return_value=None) + @patch("homeassistant.config.async_check_ha_config_file", return_value=None) def test_check_config(self, mock_check, mock_stop): """Test stop service.""" check_config(self.hass) @@ -271,8 +270,7 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): service = hass.services._services["homeassistant"]["turn_on"] with patch( - "homeassistant.core.ServiceRegistry.async_call", - side_effect=lambda *args: mock_coro(), + "homeassistant.core.ServiceRegistry.async_call", return_value=None, ) as mock_call: await service.func(service_call) @@ -296,8 +294,7 @@ async def test_entity_update(hass): await async_setup_component(hass, "homeassistant", {}) with patch( - "homeassistant.helpers.entity_component.async_update_entity", - return_value=mock_coro(), + "homeassistant.helpers.entity_component.async_update_entity", return_value=None, ) as mock_update: await hass.services.async_call( "homeassistant", diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 1371db87b3d..6234d425d8d 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,12 +1,11 @@ """Test Home Assistant scenes.""" -from unittest.mock import patch - import pytest import voluptuous as vol from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py index 6453bbbc788..4ad5f60551d 100644 --- a/tests/components/homekit/common.py +++ b/tests/components/homekit/common.py @@ -1,5 +1,7 @@ """Collection of fixtures and functions for the HomeKit tests.""" -from unittest.mock import patch +from tests.async_mock import Mock, patch + +EMPTY_8_6_JPEG = b"empty_8_6" def patch_debounce(): @@ -8,3 +10,16 @@ def patch_debounce(): "homeassistant.components.homekit.accessories.debounce", lambda f: lambda *args, **kwargs: f(*args, **kwargs), ) + + +def mock_turbo_jpeg( + first_width=None, second_width=None, first_height=None, second_height=None +): + """Mock a TurboJPEG instance.""" + mocked_turbo_jpeg = Mock() + mocked_turbo_jpeg.decode_header.side_effect = [ + (first_width, first_height, 0, 0), + (second_width, second_height, 0, 0), + ] + mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG + return mocked_turbo_jpeg diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 7093bebf9ab..2ee2e5849f7 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,12 +1,12 @@ """HomeKit session fixtures.""" -from unittest.mock import patch - from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED from homeassistant.core import callback as ha_callback +from tests.async_mock import patch + @pytest.fixture(scope="session") def hk_driver(): @@ -29,3 +29,10 @@ def events(hass): EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e)) ) yield events + + +@pytest.fixture +def mock_zeroconf(): + """Mock zeroconf.""" + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + yield mock_zc.return_value diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index c4b61f68833..092c68a5480 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,7 +3,6 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -from unittest.mock import Mock, patch import pytest @@ -15,6 +14,10 @@ from homeassistant.components.homekit.accessories import ( ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, + ATTR_INTERGRATION, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_NAME, @@ -43,6 +46,7 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import async_mock_service @@ -80,11 +84,17 @@ async def test_debounce(hass): async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" - entity_id = "homekit.accessory" + entity_id = "sensor.accessory" + entity_id2 = "light.accessory" + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id2, None) + await hass.async_block_till_done() - acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, None) + acc = HomeAccessory( + hass, hk_driver, "Home Accessory", entity_id, 2, {"platform": "isy994"} + ) assert acc.hass == hass assert acc.display_name == "Home Accessory" assert acc.aid == 2 @@ -93,26 +103,52 @@ async def test_home_accessory(hass, hk_driver): serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" - assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER - assert serv.get_characteristic(CHAR_MODEL).value == "Homekit" - assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "homekit.accessory" + assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Isy994" + assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "sensor.accessory" + + acc2 = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 3, {}) + serv = acc2.services[0] # SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" + assert serv.get_characteristic(CHAR_MANUFACTURER).value == f"{MANUFACTURER} Light" + assert serv.get_characteristic(CHAR_MODEL).value == "Light" + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory" + + acc3 = HomeAccessory( + hass, + hk_driver, + "Home Accessory", + entity_id2, + 3, + { + ATTR_MODEL: "Awesome", + ATTR_MANUFACTURER: "Lux Brands", + ATTR_SOFTWARE_VERSION: "0.4.3", + ATTR_INTERGRATION: "luxe", + }, + ) + serv = acc3.services[0] # SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" + assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Lux Brands" + assert serv.get_characteristic(CHAR_MODEL).value == "Awesome" + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory" hass.states.async_set(entity_id, "on") await hass.async_block_till_done() with patch( - "homeassistant.components.homekit.accessories.HomeAccessory.update_state" - ) as mock_update_state: + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: await acc.run_handler() await hass.async_block_till_done() state = hass.states.get(entity_id) - mock_update_state.assert_called_with(state) + mock_async_update_state.assert_called_with(state) hass.states.async_remove(entity_id) await hass.async_block_till_done() - assert mock_update_state.call_count == 1 + assert mock_async_update_state.call_count == 1 with pytest.raises(NotImplementedError): - acc.update_state("new_state") + acc.async_update_state("new_state") # Test model name from domain entity_id = "test_model.demo" @@ -130,52 +166,82 @@ async def test_battery_service(hass, hk_driver, caplog): await hass.async_block_till_done() acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None) - acc.update_state = lambda x: None assert acc._char_battery.value == 0 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) + assert acc._char_battery.value == 50 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 - hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 15 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 2 - hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: "error"}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: "error"}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 15 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 2 assert "ERROR" not in caplog.text # Test charging - hass.states.async_set( - entity_id, None, {ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True} - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set( + entity_id, None, {ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) - acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None) - acc.update_state = lambda x: None - assert acc._char_battery.value == 0 - assert acc._char_low_battery.value == 0 - assert acc._char_charging.value == 2 + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None) + assert acc._char_battery.value == 0 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 10 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 1 - hass.states.async_set( - entity_id, None, {ATTR_BATTERY_LEVEL: 100, ATTR_BATTERY_CHARGING: False} - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set( + entity_id, None, {ATTR_BATTERY_LEVEL: 100, ATTR_BATTERY_CHARGING: False} + ) + await hass.async_block_till_done() assert acc._char_battery.value == 100 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 0 @@ -197,11 +263,15 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_SENSOR: linked_battery}, ) - acc.update_state = lambda x: None assert acc.linked_battery_sensor == linked_battery - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 50 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 @@ -212,13 +282,19 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): assert acc._char_low_battery.value == 1 # Ignore battery change on entity if it has linked_battery - hass.states.async_set(entity_id, "open", {ATTR_BATTERY_LEVEL: 90}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set(entity_id, "open", {ATTR_BATTERY_LEVEL: 90}) + await hass.async_block_till_done() assert acc._char_battery.value == 10 # Test none numeric state for linked_battery - hass.states.async_set(linked_battery, "error", None) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set(linked_battery, "error", None) + await hass.async_block_till_done() assert acc._char_battery.value == 10 assert "ERROR" not in caplog.text @@ -234,15 +310,20 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_SENSOR: linked_battery, CONF_LOW_BATTERY_THRESHOLD: 50}, ) - acc.update_state = lambda x: None - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 20 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 1 hass.states.async_set(linked_battery, 100, {ATTR_BATTERY_CHARGING: False}) await hass.async_block_till_done() + state = hass.states.get(entity_id) assert acc._char_battery.value == 100 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 0 @@ -264,23 +345,37 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_CHARGING_SENSOR: linked_battery_charging_sensor}, ) - acc.update_state = lambda x: None assert acc.linked_battery_charging_sensor == linked_battery_charging_sensor - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 100 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 1 - hass.states.async_set(linked_battery_charging_sensor, STATE_OFF, None) - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(linked_battery_charging_sensor, STATE_OFF, None) + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_charging.value == 0 - hass.states.async_set(linked_battery_charging_sensor, STATE_ON, None) - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(linked_battery_charging_sensor, STATE_ON, None) + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_charging.value == 1 @@ -307,11 +402,15 @@ async def test_linked_battery_sensor_and_linked_battery_charging_sensor( CONF_LINKED_BATTERY_CHARGING_SENSOR: linked_battery_charging_sensor, }, ) - acc.update_state = lambda x: None assert acc.linked_battery_sensor == linked_battery - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 50 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 1 @@ -356,11 +455,15 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_SENSOR: linked_battery}, ) - acc.update_state = lambda x: None assert not acc.linked_battery_sensor - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert not acc.linked_battery_sensor assert acc._char_battery is None @@ -374,18 +477,23 @@ async def test_battery_appears_after_startup(hass, hk_driver, caplog): hass.states.async_set(entity_id, None, {}) await hass.async_block_till_done() - acc = HomeAccessory( - hass, hk_driver, "Accessory without battery", entity_id, 2, None - ) - acc.update_state = lambda x: None + acc = HomeAccessory(hass, hk_driver, "Accessory without battery", entity_id, 2, {}) assert acc._char_battery is None - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery is None - hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) + await hass.async_block_till_done() assert acc._char_battery is None @@ -395,7 +503,7 @@ async def test_call_service(hass, hk_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, None) + acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {}) call_service = async_mock_service(hass, "cover", "open_cover") test_domain = "cover" @@ -453,7 +561,9 @@ def test_home_driver(): pin = b"123-45-678" with patch("pyhap.accessory_driver.AccessoryDriver.__init__") as mock_driver: - driver = HomeDriver("hass", address=ip_address, port=port, persist_file=path) + driver = HomeDriver( + "hass", "entry_id", "name", address=ip_address, port=port, persist_file=path + ) mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path) driver.state = Mock(pincode=pin) @@ -467,7 +577,7 @@ def test_home_driver(): driver.pair("client_uuid", "client_public") mock_pair.assert_called_with("client_uuid", "client_public") - mock_dissmiss_msg.assert_called_with("hass") + mock_dissmiss_msg.assert_called_with("hass", "entry_id") # unpair with patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, patch( @@ -476,4 +586,4 @@ def test_home_driver(): driver.unpair("client_uuid") mock_unpair.assert_called_with("client_uuid") - mock_show_msg.assert_called_with("hass", pin, "X-HM://0") + mock_show_msg.assert_called_with("hass", "entry_id", "name", pin, "X-HM://0") diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index 12d12082a33..ff55ba9afa4 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -2,17 +2,17 @@ import os from zlib import adler32 -from asynctest import patch import pytest from homeassistant.components.homekit.aidmanager import ( - AID_MANAGER_STORAGE_KEY, AccessoryAidStorage, + get_aid_storage_filename_for_entry_id, get_system_unique_id, ) from homeassistant.helpers import device_registry from homeassistant.helpers.storage import STORAGE_DIR +from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry @@ -53,7 +53,7 @@ async def test_aid_generation(hass, device_reg, entity_reg): with patch( "homeassistant.components.homekit.aidmanager.AccessoryAidStorage.async_schedule_save" ): - aid_storage = AccessoryAidStorage(hass) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() for _ in range(0, 2): @@ -110,7 +110,7 @@ async def test_aid_adler32_collision(hass, device_reg, entity_reg): with patch( "homeassistant.components.homekit.aidmanager.AccessoryAidStorage.async_schedule_save" ): - aid_storage = AccessoryAidStorage(hass) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() seen_aids = set() @@ -129,8 +129,8 @@ async def test_aid_generation_no_unique_ids_handles_collision( hass, device_reg, entity_reg ): """Test colliding aids is stable.""" - - aid_storage = AccessoryAidStorage(hass) + config_entry = MockConfigEntry(domain="test", data={}) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() seen_aids = set() @@ -394,7 +394,7 @@ async def test_aid_generation_no_unique_ids_handles_collision( await aid_storage.async_save() await hass.async_block_till_done() - aid_storage = AccessoryAidStorage(hass) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() assert aid_storage.allocations == { @@ -620,6 +620,7 @@ async def test_aid_generation_no_unique_ids_handles_collision( "light.light99": 596247761, } - aid_storage_path = hass.config.path(STORAGE_DIR, AID_MANAGER_STORAGE_KEY) + aidstore = get_aid_storage_filename_for_entry_id(config_entry.entry_id) + aid_storage_path = hass.config.path(STORAGE_DIR, aidstore) if await hass.async_add_executor_job(os.path.exists, aid_storage_path): await hass.async_add_executor_job(os.unlink, aid_storage_path) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py new file mode 100644 index 00000000000..cb4d81408cb --- /dev/null +++ b/tests/components/homekit/test_config_flow.py @@ -0,0 +1,373 @@ +"""Test the HomeKit config flow.""" +from asynctest import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.homekit.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME, CONF_PORT + +from tests.common import MockConfigEntry + + +def _mock_config_entry_with_options_populated(): + """Create a mock config entry with options populated.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "filter": { + "include_domains": [ + "fan", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "exclude_entities": ["climate.front_gate"], + }, + "auto_start": False, + "safe_mode": False, + "zeroconf_default_interface": True, + }, + ) + + +async def test_user_form(hass): + """Test we can setup a new instance.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.homekit.config_flow.find_next_available_port", + return_value=12345, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"auto_start": True, "include_domains": ["light"]}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "pairing" + + with patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"][:11] == "HASS Bridge" + bridge_name = (result3["title"].split(":"))[0] + assert result3["data"] == { + "auto_start": True, + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["light"], + "include_entities": [], + }, + "name": bridge_name, + "port": 12345, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass): + """Test we can import instance.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "port_name_in_use" + + with patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_NAME: "othername", CONF_PORT: 56789}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "othername:56789" + assert result2["data"] == { + "name": "othername", + "port": 56789, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_options_flow_advanced(hass): + """Test config flow options.""" + + config_entry = _mock_config_entry_with_options_populated() + config_entry.add_to_hass(hass) + + hass.states.async_set("climate.old", "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"include_domains": ["fan", "vacuum", "climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"exclude_entities": ["climate.old"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "advanced" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={ + "auto_start": True, + "safe_mode": True, + "zeroconf_default_interface": False, + }, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": True, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old"], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": [], + }, + "safe_mode": True, + "zeroconf_default_interface": False, + } + + +async def test_options_flow_basic(hass): + """Test config flow options.""" + + config_entry = _mock_config_entry_with_options_populated() + config_entry.add_to_hass(hass) + + hass.states.async_set("climate.old", "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"include_domains": ["fan", "vacuum", "climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"exclude_entities": ["climate.old"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "advanced" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"safe_mode": True, "zeroconf_default_interface": False}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": False, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old"], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": [], + }, + "safe_mode": True, + "zeroconf_default_interface": False, + } + + +async def test_options_flow_with_cameras(hass): + """Test config flow options.""" + + config_entry = _mock_config_entry_with_options_populated() + config_entry.add_to_hass(hass) + + hass.states.async_set("climate.old", "off") + hass.states.async_set("camera.native_h264", "off") + hass.states.async_set("camera.transcode_h264", "off") + hass.states.async_set("camera.excluded", "off") + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"include_domains": ["fan", "vacuum", "climate", "camera"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"exclude_entities": ["climate.old", "camera.excluded"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "cameras" + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], user_input={"camera_copy": ["camera.native_h264"]}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "advanced" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={"safe_mode": True, "zeroconf_default_interface": False}, + ) + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": False, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old", "camera.excluded"], + "include_domains": ["fan", "vacuum", "climate", "camera"], + "include_entities": [], + }, + "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, + "safe_mode": True, + "zeroconf_default_interface": False, + } + + # Now run though again and verify we can turn off copy + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"include_domains": ["fan", "vacuum", "climate", "camera"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"exclude_entities": ["climate.old", "camera.excluded"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "cameras" + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], user_input={"camera_copy": []}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "advanced" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={"safe_mode": True, "zeroconf_default_interface": False}, + ) + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": False, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old", "camera.excluded"], + "include_domains": ["fan", "vacuum", "climate", "camera"], + "include_entities": [], + }, + "entity_config": {"camera.native_h264": {}}, + "safe_mode": True, + "zeroconf_default_interface": False, + } + + +async def test_options_flow_blocked_when_from_yaml(hass): + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "auto_start": True, + "filter": { + "include_domains": [ + "fan", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "exclude_entities": ["climate.front_gate"], + }, + "safe_mode": False, + "zeroconf_default_interface": True, + }, + source=SOURCE_IMPORT, + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "yaml" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 08a04d5b88e..11827c2ce4f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,12 +1,11 @@ """Package to test the get_accessory method.""" -from unittest.mock import Mock, patch - import pytest import homeassistant.components.climate as climate import homeassistant.components.cover as cover -from homeassistant.components.homekit import TYPES, get_accessory +from homeassistant.components.homekit.accessories import TYPES, get_accessory from homeassistant.components.homekit.const import ( + ATTR_INTERGRATION, CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_FAUCET, @@ -17,6 +16,7 @@ from homeassistant.components.homekit.const import ( TYPE_VALVE, ) import homeassistant.components.media_player.const as media_player_c +import homeassistant.components.vacuum as vacuum from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -30,6 +30,8 @@ from homeassistant.const import ( ) from homeassistant.core import State +from tests.async_mock import Mock, patch + def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" @@ -60,10 +62,12 @@ def test_not_supported_media_player(): def test_customize_options(config, name): """Test with customized options.""" mock_type = Mock() + conf = config.copy() + conf[ATTR_INTERGRATION] = "platform_name" with patch.dict(TYPES, {"Light": mock_type}): entity_state = State("light.demo", "on") - get_accessory(None, None, entity_state, 2, config) - mock_type.assert_called_with(None, None, name, "light.demo", 2, config) + get_accessory(None, None, entity_state, 2, conf) + mock_type.assert_called_with(None, None, name, "light.demo", 2, conf) @pytest.mark.parametrize( @@ -239,3 +243,39 @@ def test_type_switches(type_name, entity_id, state, attrs, config): entity_state = State(entity_id, state, attrs) get_accessory(None, None, entity_state, 2, config) assert mock_type.called + + +@pytest.mark.parametrize( + "type_name, entity_id, state, attrs", + [ + ( + "DockVacuum", + "vacuum.dock_vacuum", + "docked", + { + ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_START + | vacuum.SUPPORT_RETURN_HOME + }, + ), + ("Switch", "vacuum.basic_vacuum", "off", {}), + ], +) +def test_type_vacuum(type_name, entity_id, state, attrs): + """Test if vacuum types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, None, entity_state, 2, {}) + assert mock_type.called + + +@pytest.mark.parametrize( + "type_name, entity_id, state, attrs", [("Camera", "camera.basic", "on", {})], +) +def test_type_camera(type_name, entity_id, state, attrs): + """Test if camera types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, None, entity_state, 2, {}) + assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 7ba51106cb9..8a4ac87f21b 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,11 +1,11 @@ """Tests for the HomeKit component.""" -from unittest.mock import ANY, Mock, patch +import os +from typing import Dict -from asynctest import CoroutineMock import pytest from zeroconf import InterfaceChoice -from homeassistant import setup +from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING from homeassistant.components.homekit import ( MAX_DEVICES, @@ -20,6 +20,7 @@ from homeassistant.components.homekit.const import ( AID_STORAGE, BRIDGE_NAME, CONF_AUTO_START, + CONF_ENTRY_INDEX, CONF_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE, DEFAULT_PORT, @@ -29,6 +30,11 @@ from homeassistant.components.homekit.const import ( SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, ) +from homeassistant.components.homekit.util import ( + get_aid_storage_fullpath_for_entry_id, + get_persist_fullpath_for_entry_id, +) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -43,12 +49,17 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.helpers import device_registry from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.setup import async_setup_component +from homeassistant.util import json as json_util +from .util import PATH_HOMEKIT, async_init_entry, async_init_integration + +from tests.async_mock import ANY, AsyncMock, Mock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce IP_ADDRESS = "127.0.0.1" -PATH_HOMEKIT = "homeassistant.components.homekit" @pytest.fixture @@ -73,11 +84,32 @@ def debounce_patcher(): async def test_setup_min(hass): """Test async_setup with min config options.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + entry.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + type(homekit).async_setup_zeroconf = AsyncMock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mock_homekit.assert_any_call( - hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None, None + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + None, + entry.entry_id, ) assert mock_homekit().setup.called is True @@ -86,26 +118,28 @@ async def test_setup_min(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - mock_homekit().async_start.assert_called_with(ANY) + mock_homekit().async_start.assert_called() async def test_setup_auto_start_disabled(hass): """Test async_setup with auto start disabled and test service calls.""" - config = { - DOMAIN: { + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, + options={ CONF_AUTO_START: False, - CONF_NAME: "Test Name", - CONF_PORT: 11111, - CONF_IP_ADDRESS: "172.0.0.0", CONF_SAFE_MODE: DEFAULT_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE: True, - } - } + }, + ) + entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() - type(homekit).async_start = CoroutineMock() - assert await setup.async_setup_component(hass, DOMAIN, config) + type(homekit).async_start = AsyncMock() + type(homekit).async_setup_zeroconf = AsyncMock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mock_homekit.assert_any_call( hass, @@ -117,6 +151,7 @@ async def test_setup_auto_start_disabled(hass): DEFAULT_SAFE_MODE, None, InterfaceChoice.Default, + entry.entry_id, ) assert mock_homekit().setup.called is True @@ -133,6 +168,7 @@ async def test_setup_auto_start_disabled(hass): homekit.status = STATUS_READY await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + await hass.async_block_till_done() assert homekit.async_start.called is True # Test start call with driver started @@ -141,12 +177,29 @@ async def test_setup_auto_start_disabled(hass): homekit.status = STATUS_STOPPED await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + await hass.async_block_till_done() assert homekit.async_start.called is False async def test_homekit_setup(hass, hk_driver): """Test setup of bridge and driver.""" - homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, DEFAULT_SAFE_MODE) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) + homekit = HomeKit( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + {}, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver @@ -154,10 +207,12 @@ async def test_homekit_setup(hass, hk_driver): mock_ip.return_value = IP_ADDRESS await hass.async_add_executor_job(homekit.setup) - path = hass.config.path(HOMEKIT_FILE) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path, @@ -172,17 +227,36 @@ async def test_homekit_setup(hass, hk_driver): async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" - homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", {}, {}, None) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) + homekit = HomeKit( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + "172.0.0.0", + {}, + {}, + None, + None, + interface_choice=None, + entry_id=entry.entry_id, + ) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address="172.0.0.0", port=DEFAULT_PORT, - persist_file=ANY, + persist_file=path, advertised_address=None, interface_choice=None, ) @@ -190,19 +264,36 @@ async def test_homekit_setup_ip_address(hass, hk_driver): async def test_homekit_setup_advertise_ip(hass, hk_driver): """Test setup with given IP address to advertise.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) homekit = HomeKit( - hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" + hass, + BRIDGE_NAME, + DEFAULT_PORT, + "0.0.0.0", + {}, + {}, + None, + "192.168.1.100", + interface_choice=None, + entry_id=entry.entry_id, ) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address="0.0.0.0", port=DEFAULT_PORT, - persist_file=ANY, + persist_file=path, advertised_address="192.168.1.100", interface_choice=None, ) @@ -210,6 +301,11 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): async def test_homekit_setup_interface_choice(hass, hk_driver): """Test setup with interface choice of Default.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) homekit = HomeKit( hass, BRIDGE_NAME, @@ -220,17 +316,21 @@ async def test_homekit_setup_interface_choice(hass, hk_driver): None, None, InterfaceChoice.Default, + entry_id=entry.entry_id, ) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address="0.0.0.0", port=DEFAULT_PORT, - persist_file=ANY, + persist_file=path, advertised_address=None, interface_choice=InterfaceChoice.Default, ) @@ -238,7 +338,23 @@ async def test_homekit_setup_interface_choice(hass, hk_driver): async def test_homekit_setup_safe_mode(hass, hk_driver): """Test if safe_mode flag is set.""" - homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, True, None) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) + homekit = HomeKit( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + {}, + {}, + True, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver): await hass.async_add_executor_job(homekit.setup) @@ -247,12 +363,25 @@ async def test_homekit_setup_safe_mode(hass, hk_driver): async def test_homekit_add_accessory(hass): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(hass, None, None, None, lambda entity_id: True, {}, None, None) + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + lambda entity_id: True, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_init_integration(hass) with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, "acc", None] @@ -271,7 +400,20 @@ async def test_homekit_add_accessory(hass): async def test_homekit_remove_accessory(hass): """Remove accessory from bridge.""" - homekit = HomeKit("hass", None, None, None, lambda entity_id: True, {}, None, None) + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + lambda entity_id: True, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() mock_bridge.accessories = {"light.demo": "acc"} @@ -283,10 +425,21 @@ async def test_homekit_remove_accessory(hass): async def test_homekit_entity_filter(hass): """Test the entity filter.""" - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entry = await async_init_integration(hass) entity_filter = generate_filter(["cover"], ["demo.test"], [], []) - homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + entity_filter, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -307,8 +460,21 @@ async def test_homekit_entity_filter(hass): async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" + entry = await async_init_integration(hass) + pin = b"123-45-678" - homekit = HomeKit(hass, None, None, None, {}, {"cover.demo": {}}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver @@ -326,8 +492,9 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): ) as hk_driver_start: await homekit.async_start() + await hass.async_block_till_done() mock_add_acc.assert_called_with(state) - mock_setup_msg.assert_called_with(hass, pin, ANY) + mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -335,17 +502,32 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): # Test start() if already started hk_driver_start.reset_mock() await homekit.async_start() + await hass.async_block_till_done() assert not hk_driver_start.called async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b"123-45-678" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_init_entry(hass, entry) + homekit = HomeKit( + hass, + None, + None, + None, + entity_filter, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) - homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver @@ -362,7 +544,8 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p ) as hk_driver_start: await homekit.async_start() - mock_setup_msg.assert_called_with(hass, pin, ANY) + await hass.async_block_till_done() + mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -370,15 +553,29 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p # Test start() if already started hk_driver_start.reset_mock() await homekit.async_start() + await hass.async_block_till_done() assert not hk_driver_start.called async def test_homekit_stop(hass): """Test HomeKit stop method.""" - homekit = HomeKit(hass, None, None, None, None, None, None) + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = Mock() - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_init_integration(hass) assert homekit.status == STATUS_READY await homekit.async_stop() @@ -400,8 +597,23 @@ async def test_homekit_stop(hass): async def test_homekit_reset_accessories(hass): """Test adding too many accessories to HomeKit.""" + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) entity_id = "light.demo" - homekit = HomeKit(hass, None, None, None, {}, {entity_id: {}}, None) + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {entity_id: {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -409,11 +621,14 @@ async def test_homekit_reset_accessories(hass): f"{PATH_HOMEKIT}.HomeKit.setup" ), patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, patch( "pyhap.accessory_driver.AccessoryDriver.config_changed" - ) as hk_driver_config_changed: + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.start" + ): + await async_init_entry(hass, entry) - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - aid = hass.data[AID_STORAGE].get_or_allocate_aid_for_entity_id(entity_id) + aid = hass.data[DOMAIN][entry.entry_id][ + AID_STORAGE + ].get_or_allocate_aid_for_entity_id(entity_id) homekit.bridge.accessories = {aid: "acc"} homekit.status = STATUS_RUNNING @@ -432,10 +647,22 @@ async def test_homekit_reset_accessories(hass): async def test_homekit_too_many_accessories(hass, hk_driver): """Test adding too many accessories to HomeKit.""" + entry = await async_init_integration(hass) entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) - homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + entity_filter, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() # The bridge itself counts as an accessory homekit.bridge.accessories = range(MAX_DEVICES) @@ -457,9 +684,20 @@ async def test_homekit_finds_linked_batteries( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test HomeKit start method.""" - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entry = await async_init_integration(hass) - homekit = HomeKit(hass, None, None, None, {}, {"light.demo": {}}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {"light.demo": {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = hk_driver homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") @@ -468,25 +706,28 @@ async def test_homekit_finds_linked_batteries( config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, + sw_version="0.16.0", + model="Powerwall 2", + manufacturer="Tesla", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) binary_charging_sensor = entity_reg.async_get_or_create( "binary_sensor", - "light", + "powerwall", "battery_charging", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY_CHARGING, ) battery_sensor = entity_reg.async_get_or_create( "sensor", - "light", + "powerwall", "battery", device_id=device_entry.id, device_class=DEVICE_CLASS_BATTERY, ) light = entity_reg.async_get_or_create( - "light", "light", "demo", device_id=device_entry.id + "light", "powerwall", "demo", device_id=device_entry.id ) hass.states.async_set( @@ -508,6 +749,7 @@ async def test_homekit_finds_linked_batteries( "pyhap.accessory_driver.AccessoryDriver.start" ): await homekit.async_start() + await hass.async_block_till_done() mock_get_acc.assert_called_with( hass, @@ -515,7 +757,159 @@ async def test_homekit_finds_linked_batteries( ANY, ANY, { - "linked_battery_charging_sensor": "binary_sensor.light_battery_charging", - "linked_battery_sensor": "sensor.light_battery", + "manufacturer": "Tesla", + "model": "Powerwall 2", + "sw_version": "0.16.0", + "linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging", + "linked_battery_sensor": "sensor.powerwall_battery", }, ) + + +async def test_setup_imported(hass): + """Test async_setup with imported config options.""" + legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) + legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") + legacy_homekit_state_contents = {"homekit.state": 1} + legacy_homekit_aids_contents = {"homekit.aids": 1} + await hass.async_add_executor_job( + _write_data, legacy_persist_file_path, legacy_homekit_state_contents + ) + await hass.async_add_executor_job( + _write_data, legacy_aid_storage_path, legacy_homekit_aids_contents + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT, CONF_ENTRY_INDEX: 0}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + type(homekit).async_setup_zeroconf = AsyncMock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_homekit.assert_any_call( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + None, + entry.entry_id, + ) + assert mock_homekit().setup.called is True + + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + mock_homekit().async_start.assert_called() + + migrated_persist_file_path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) + assert ( + await hass.async_add_executor_job( + json_util.load_json, migrated_persist_file_path + ) + == legacy_homekit_state_contents + ) + os.unlink(migrated_persist_file_path) + migrated_aid_file_path = get_aid_storage_fullpath_for_entry_id(hass, entry.entry_id) + assert ( + await hass.async_add_executor_job(json_util.load_json, migrated_aid_file_path) + == legacy_homekit_aids_contents + ) + os.unlink(migrated_aid_file_path) + + +async def test_yaml_updates_update_config_entry_for_name(hass): + """Test async_setup with imported config.""" + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + type(homekit).async_setup_zeroconf = AsyncMock() + assert await async_setup_component( + hass, "homekit", {"homekit": {CONF_NAME: BRIDGE_NAME, CONF_PORT: 12345}} + ) + await hass.async_block_till_done() + + mock_homekit.assert_any_call( + hass, + BRIDGE_NAME, + 12345, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + None, + entry.entry_id, + ) + assert mock_homekit().setup.called is True + + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + mock_homekit().async_start.assert_called() + + +async def test_raise_config_entry_not_ready(hass): + """Test async_setup when the port is not available.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homekit.port_is_available", return_value=False, + ): + assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): + """Test HomeKit uses system zeroconf.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + system_zc = await zeroconf.async_get_instance(hass) + + with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver), patch( + f"{PATH_HOMEKIT}.HomeKit.async_start" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert hk_driver.advertiser == system_zc + + +def _write_data(path: str, data: Dict) -> None: + """Write the data.""" + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + json_util.save_json(path, data) diff --git a/tests/components/homekit/test_img_util.py b/tests/components/homekit/test_img_util.py new file mode 100644 index 00000000000..4ada89b3acd --- /dev/null +++ b/tests/components/homekit/test_img_util.py @@ -0,0 +1,62 @@ +"""Test HomeKit img_util module.""" +from homeassistant.components.camera import Image +from homeassistant.components.homekit.img_util import ( + TurboJPEGSingleton, + scale_jpeg_camera_image, +) + +from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg + +from tests.async_mock import patch + +EMPTY_16_12_JPEG = b"empty_16_12" + + +def test_turbojpeg_singleton(): + """Verify the instance always gives back the same.""" + assert TurboJPEGSingleton.instance() == TurboJPEGSingleton.instance() + + +def test_scale_jpeg_camera_image(): + """Test we can scale a jpeg image.""" + + camera_image = Image("image/jpeg", EMPTY_16_12_JPEG) + + turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) + with patch( + "homeassistant.components.homekit.img_util.TurboJPEG", return_value=False + ): + TurboJPEGSingleton() + assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content + + turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) + with patch( + "homeassistant.components.homekit.img_util.TurboJPEG", return_value=turbo_jpeg + ): + TurboJPEGSingleton() + assert scale_jpeg_camera_image(camera_image, 16, 12) == EMPTY_16_12_JPEG + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=8, second_height=6 + ) + with patch( + "homeassistant.components.homekit.img_util.TurboJPEG", return_value=turbo_jpeg + ): + TurboJPEGSingleton() + jpeg_bytes = scale_jpeg_camera_image(camera_image, 8, 6) + + assert jpeg_bytes == EMPTY_8_6_JPEG + + +def test_turbojpeg_load_failure(): + """Handle libjpegturbo not being installed.""" + + with patch( + "homeassistant.components.homekit.img_util.TurboJPEG", side_effect=Exception + ): + TurboJPEGSingleton() + assert TurboJPEGSingleton.instance() is False + + with patch("homeassistant.components.homekit.img_util.TurboJPEG"): + TurboJPEGSingleton() + assert TurboJPEGSingleton.instance() diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index e01588305d5..6d01413da8f 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -1,6 +1,4 @@ """Test HomeKit initialization.""" -from asynctest import patch - from homeassistant import core as ha from homeassistant.components import logbook from homeassistant.components.homekit.const import ( @@ -12,6 +10,8 @@ from homeassistant.components.homekit.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.setup import async_setup_component +from tests.async_mock import patch + async def test_humanify_homekit_changed_event(hass, hk_driver): """Test humanifying HomeKit changed event.""" diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py new file mode 100644 index 00000000000..e3444ca23e4 --- /dev/null +++ b/tests/components/homekit/test_type_cameras.py @@ -0,0 +1,496 @@ +"""Test different accessory types: Camera.""" + +from uuid import UUID + +from pyhap.accessory_driver import AccessoryDriver +import pytest + +from homeassistant.components import camera, ffmpeg +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import ( + AUDIO_CODEC_COPY, + CONF_AUDIO_CODEC, + CONF_STREAM_SOURCE, + CONF_SUPPORT_AUDIO, + CONF_VIDEO_CODEC, + VIDEO_CODEC_COPY, + VIDEO_CODEC_H264_OMX, +) +from homeassistant.components.homekit.img_util import TurboJPEGSingleton +from homeassistant.components.homekit.type_cameras import Camera +from homeassistant.components.homekit.type_switches import Switch +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from .common import mock_turbo_jpeg + +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch + +MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" +MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" +MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6") + +PID_THAT_WILL_NEVER_BE_ALIVE = 2147483647 + + +async def _async_start_streaming(hass, acc): + """Start streaming a camera.""" + acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) + await acc.run_handler() + await hass.async_block_till_done() + + +async def _async_setup_endpoints(hass, acc): + """Set camera endpoints.""" + acc.set_endpoints(MOCK_END_POINTS_TLV) + await acc.run_handler() + await hass.async_block_till_done() + + +async def _async_reconfigure_stream(hass, acc, session_info, stream_config): + """Reconfigure the stream.""" + await acc.reconfigure_stream(session_info, stream_config) + await acc.run_handler() + await hass.async_block_till_done() + + +async def _async_stop_all_streams(hass, acc): + """Stop all camera streams.""" + await acc.stop() + await acc.run_handler() + await hass.async_block_till_done() + + +async def _async_stop_stream(hass, acc, session_info): + """Stop a camera stream.""" + await acc.stop_stream(session_info) + await acc.run_handler() + await hass.async_block_till_done() + + +@pytest.fixture() +def run_driver(hass): + """Return a custom AccessoryDriver instance for HomeKit accessory init.""" + with patch("pyhap.accessory_driver.Zeroconf"), patch( + "pyhap.accessory_driver.AccessoryEncoder" + ), patch("pyhap.accessory_driver.HAPServer"), patch( + "pyhap.accessory_driver.AccessoryDriver.publish" + ), patch( + "pyhap.accessory_driver.AccessoryDriver.persist" + ): + yield AccessoryDriver( + pincode=b"123-45-678", address="127.0.0.1", loop=hass.loop + ) + + +def _get_exits_after_startup_mock_ffmpeg(): + """Return a ffmpeg that will have an invalid pid.""" + ffmpeg = MagicMock() + type(ffmpeg.process).pid = PropertyMock(return_value=PID_THAT_WILL_NEVER_BE_ALIVE) + ffmpeg.open = AsyncMock(return_value=True) + ffmpeg.close = AsyncMock(return_value=True) + ffmpeg.kill = AsyncMock(return_value=True) + return ffmpeg + + +def _get_working_mock_ffmpeg(): + """Return a working ffmpeg.""" + ffmpeg = MagicMock() + ffmpeg.open = AsyncMock(return_value=True) + ffmpeg.close = AsyncMock(return_value=True) + ffmpeg.kill = AsyncMock(return_value=True) + return ffmpeg + + +def _get_failing_mock_ffmpeg(): + """Return an ffmpeg that fails to shutdown.""" + ffmpeg = MagicMock() + type(ffmpeg.process).pid = PropertyMock(return_value=PID_THAT_WILL_NEVER_BE_ALIVE) + ffmpeg.open = AsyncMock(return_value=False) + ffmpeg.close = AsyncMock(side_effect=OSError) + ffmpeg.kill = AsyncMock(side_effect=OSError) + return ffmpeg + + +async def test_camera_stream_source_configured(hass, run_driver, events): + """Test a camera that can stream with a configured source.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {CONF_STREAM_SOURCE: "/dev/null", CONF_SUPPORT_AUDIO: True}, + ) + not_camera_acc = Switch(hass, run_driver, "Switch", entity_id, 4, {},) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + bridge.add_accessory(not_camera_acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + working_ffmpeg = _get_working_mock_ffmpeg() + session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ): + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + + expected_output = ( + "-map 0:v:0 -an -c:v libx264 -profile:v high -tune zerolatency -pix_fmt " + "yuv420p -r 30 -b:v 299k -bufsize 1196k -maxrate 299k -payload_type 99 -ssrc {v_ssrc} -f " + "rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params " + "zdPmNLWeI86DtLJHvVLI6YPvqhVeeiLsNtrAgbgL " + "srtp://192.168.208.5:51246?rtcpport=51246&localrtcpport=51246&pkt_size=1316 -map 0:a:0 " + "-vn -c:a libopus -application lowdelay -ac 1 -ar 24k -b:a 24k -bufsize 96k -payload_type " + "110 -ssrc {a_ssrc} -f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params " + "shnETgfD+7xUQ8zRdsaytY11wu6CO73IJ+RZVJpU " + "srtp://192.168.208.5:51108?rtcpport=51108&localrtcpport=51108&pkt_size=188" + ) + + working_ffmpeg.open.assert_called_with( + cmd=[], + input_source="-i /dev/null", + output=expected_output.format(**session_info), + stdout_pipe=False, + ) + + await _async_setup_endpoints(hass, acc) + working_ffmpeg = _get_working_mock_ffmpeg() + session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ): + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + # Calling a second time should not throw + await _async_stop_all_streams(hass, acc) + + turbo_jpeg = mock_turbo_jpeg( + first_width=16, first_height=12, second_width=300, second_height=200 + ) + with patch( + "homeassistant.components.homekit.img_util.TurboJPEG", return_value=turbo_jpeg + ): + TurboJPEGSingleton() + assert await hass.async_add_executor_job( + acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} + ) + # Verify the bridge only forwards get_snapshot for + # cameras and valid accessory ids + assert await hass.async_add_executor_job( + bridge.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} + ) + + with pytest.raises(ValueError): + assert await hass.async_add_executor_job( + bridge.get_snapshot, {"aid": 3, "image-width": 300, "image-height": 200} + ) + with pytest.raises(ValueError): + assert await hass.async_add_executor_job( + bridge.get_snapshot, {"aid": 4, "image-width": 300, "image-height": 200} + ) + + +async def test_camera_stream_source_configured_with_failing_ffmpeg( + hass, run_driver, events +): + """Test a camera that can stream with a configured source with ffmpeg failing.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {CONF_STREAM_SOURCE: "/dev/null", CONF_SUPPORT_AUDIO: True}, + ) + not_camera_acc = Switch(hass, run_driver, "Switch", entity_id, 4, {},) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + bridge.add_accessory(not_camera_acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_failing_mock_ffmpeg(), + ): + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + # Calling a second time should not throw + await _async_stop_all_streams(hass, acc) + + +async def test_camera_stream_source_found(hass, run_driver, events): + """Test a camera that can stream and we get the source from the entity.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + + await _async_setup_endpoints(hass, acc) + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + + +async def test_camera_stream_source_fails(hass, run_driver, events): + """Test a camera that can stream and we cannot get the source from the entity.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + side_effect=OSError, + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + + +async def test_camera_with_no_stream(hass, run_driver, events): + """Test a camera that cannot stream.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, camera.DOMAIN, {camera.DOMAIN: {}}) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + await _async_start_streaming(hass, acc) + await _async_stop_all_streams(hass, acc) + + with pytest.raises(HomeAssistantError): + await hass.async_add_executor_job( + acc.get_snapshot, {"aid": 2, "image-width": 300, "image-height": 200} + ) + + +async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, events): + """Test a camera that can stream with a configured source.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + { + CONF_STREAM_SOURCE: "/dev/null", + CONF_SUPPORT_AUDIO: True, + CONF_VIDEO_CODEC: VIDEO_CODEC_COPY, + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + + working_ffmpeg = _get_working_mock_ffmpeg() + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ): + await _async_start_streaming(hass, acc) + await _async_reconfigure_stream(hass, acc, session_info, {}) + await _async_stop_stream(hass, acc, session_info) + await _async_stop_all_streams(hass, acc) + + expected_output = ( + "-map 0:v:0 -an -c:v copy -tune zerolatency -pix_fmt yuv420p -r 30 -b:v 299k " + "-bufsize 1196k -maxrate 299k -payload_type 99 -ssrc {v_ssrc} -f rtp -srtp_out_suite " + "AES_CM_128_HMAC_SHA1_80 -srtp_out_params zdPmNLWeI86DtLJHvVLI6YPvqhVeeiLsNtrAgbgL " + "srtp://192.168.208.5:51246?rtcpport=51246&localrtcpport=51246&pkt_size=1316 -map 0:a:0 " + "-vn -c:a copy -ac 1 -ar 24k -b:a 24k -bufsize 96k -payload_type 110 -ssrc {a_ssrc} " + "-f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params " + "shnETgfD+7xUQ8zRdsaytY11wu6CO73IJ+RZVJpU " + "srtp://192.168.208.5:51108?rtcpport=51108&localrtcpport=51108&pkt_size=188" + ) + + working_ffmpeg.open.assert_called_with( + cmd=[], + input_source="-i /dev/null", + output=expected_output.format(**session_info), + stdout_pipe=False, + ) + + +async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, events): + """Test a camera that can stream with a configured source.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + { + CONF_STREAM_SOURCE: "/dev/null", + CONF_SUPPORT_AUDIO: True, + CONF_VIDEO_CODEC: VIDEO_CODEC_H264_OMX, + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + await _async_setup_endpoints(hass, acc) + session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + + ffmpeg_with_invalid_pid = _get_exits_after_startup_mock_ffmpeg() + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=ffmpeg_with_invalid_pid, + ): + await _async_start_streaming(hass, acc) + await _async_reconfigure_stream(hass, acc, session_info, {}) + # Should not throw + await _async_stop_stream(hass, acc, {"id": "does_not_exist"}) + await _async_stop_all_streams(hass, acc) + + expected_output = ( + "-map 0:v:0 -an -c:v h264_omx -profile:v high -tune zerolatency -pix_fmt yuv420p -r 30 -b:v 299k " + "-bufsize 1196k -maxrate 299k -payload_type 99 -ssrc {v_ssrc} -f rtp -srtp_out_suite " + "AES_CM_128_HMAC_SHA1_80 -srtp_out_params zdPmNLWeI86DtLJHvVLI6YPvqhVeeiLsNtrAgbgL " + "srtp://192.168.208.5:51246?rtcpport=51246&localrtcpport=51246&pkt_size=1316 -map 0:a:0 " + "-vn -c:a copy -ac 1 -ar 24k -b:a 24k -bufsize 96k -payload_type 110 -ssrc {a_ssrc} " + "-f rtp -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params " + "shnETgfD+7xUQ8zRdsaytY11wu6CO73IJ+RZVJpU " + "srtp://192.168.208.5:51108?rtcpport=51108&localrtcpport=51108&pkt_size=188" + ) + + ffmpeg_with_invalid_pid.open.assert_called_with( + cmd=[], + input_source="-i /dev/null", + output=expected_output.format(**session_info), + stdout_pipe=False, + ) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 4d2ace24eab..6d5b0f9841b 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,6 +1,5 @@ """Test different accessory types: Fans.""" from collections import namedtuple -from unittest.mock import Mock from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -33,6 +32,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import entity_registry +from tests.async_mock import Mock from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 50385bdd880..bd533417121 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -219,6 +219,14 @@ async def test_media_player_television(hass, hk_driver, events, caplog): await hass.async_block_till_done() assert acc.char_active.value == 0 + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + hass.states.async_set(entity_id, STATE_STANDBY) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + hass.states.async_set(entity_id, STATE_ON, {ATTR_INPUT_SOURCE: "HDMI 2"}) await hass.async_block_till_done() assert acc.char_input_source.value == 1 @@ -348,6 +356,7 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) await acc.run_handler() + await hass.async_block_till_done() assert acc.chars_tv == [] assert acc.chars_speaker == [] diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 690cd8f318f..b139fac3657 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -28,6 +28,7 @@ async def test_switch_set_state(hass, hk_driver, events): await hass.async_block_till_done() acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) await acc.run_handler() + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 11 # AlarmSystem diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 82abed32c0e..8a303ede876 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,6 +1,5 @@ """Test different accessory types: Thermostats.""" from collections import namedtuple -from unittest.mock import patch from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -56,6 +55,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import entity_registry +from tests.async_mock import patch from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 45ad042238f..48f0a6d270e 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,9 +7,10 @@ from homeassistant.components.homekit.const import ( CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + DEFAULT_CONFIG_FLOW_PORT, + DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - HOMEKIT_NOTIFY_ID, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, TYPE_FAUCET, @@ -26,6 +27,9 @@ from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, + find_next_available_port, + format_sw_version, + port_is_available, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -35,7 +39,7 @@ from homeassistant.components.homekit.util import ( from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, - DOMAIN, + DOMAIN as PERSISTENT_NOTIFICATION_DOMAIN, ) from homeassistant.const import ( ATTR_CODE, @@ -48,6 +52,8 @@ from homeassistant.const import ( ) from homeassistant.core import State +from .util import async_init_integration + from tests.common import async_mock_service @@ -213,27 +219,36 @@ async def test_show_setup_msg(hass): """Test show setup message as persistence notification.""" pincode = b"123-45-678" - call_create_notification = async_mock_service(hass, DOMAIN, "create") + entry = await async_init_integration(hass) + assert entry - await hass.async_add_executor_job(show_setup_message, hass, pincode, "X-HM://0") + call_create_notification = async_mock_service( + hass, PERSISTENT_NOTIFICATION_DOMAIN, "create" + ) + + await hass.async_add_executor_job( + show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" + ) await hass.async_block_till_done() - assert hass.data[HOMEKIT_PAIRING_QR_SECRET] - assert hass.data[HOMEKIT_PAIRING_QR] + assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR_SECRET] + assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR] assert call_create_notification - assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID + assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == entry.entry_id assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE] async def test_dismiss_setup_msg(hass): """Test dismiss setup message.""" - call_dismiss_notification = async_mock_service(hass, DOMAIN, "dismiss") + call_dismiss_notification = async_mock_service( + hass, PERSISTENT_NOTIFICATION_DOMAIN, "dismiss" + ) - await hass.async_add_executor_job(dismiss_setup_message, hass) + await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id") await hass.async_block_till_done() assert call_dismiss_notification - assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID + assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == "entry_id" def test_homekit_speed_mapping(): @@ -291,3 +306,22 @@ def test_speed_to_states(): assert speed_mapping.speed_to_states(66) == "low" assert speed_mapping.speed_to_states(67) == "high" assert speed_mapping.speed_to_states(100) == "high" + + +async def test_port_is_available(hass): + """Test we can get an available port and it is actually available.""" + next_port = await hass.async_add_executor_job( + find_next_available_port, DEFAULT_CONFIG_FLOW_PORT + ) + assert next_port + + assert await hass.async_add_executor_job(port_is_available, next_port) + + +async def test_format_sw_version(): + """Test format_sw_version method.""" + assert format_sw_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8" + assert format_sw_version("undefined-undefined-1.6.8") == "1.6.8" + assert format_sw_version("56.0-76060") == "56.0.76060" + assert format_sw_version(3.6) == "3.6" + assert format_sw_version("unknown") is None diff --git a/tests/components/homekit/util.py b/tests/components/homekit/util.py new file mode 100644 index 00000000000..0abf3007c04 --- /dev/null +++ b/tests/components/homekit/util.py @@ -0,0 +1,34 @@ +"""Test util for the homekit integration.""" + +from asynctest import patch + +from homeassistant.components.homekit.const import DOMAIN +from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +PATH_HOMEKIT = "homeassistant.components.homekit" + + +async def async_init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the homekit integration in Home Assistant.""" + + with patch(f"{PATH_HOMEKIT}.HomeKit.async_start"): + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry + + +async def async_init_entry(hass: HomeAssistant, entry: MockConfigEntry): + """Set up the homekit integration in Home Assistant.""" + + with patch(f"{PATH_HOMEKIT}.HomeKit.async_start"): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 99e86335cdb..ac4a0b4b5d6 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -3,9 +3,10 @@ import datetime from unittest import mock from aiohomekit.testing import FakeController -import asynctest import pytest +import tests.async_mock + @pytest.fixture def utcnow(request): @@ -20,5 +21,5 @@ def utcnow(request): def controller(hass): """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" instance = FakeController() - with asynctest.patch("aiohomekit.Controller", return_value=instance): + with tests.async_mock.patch("aiohomekit.Controller", return_value=instance): yield instance diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 302104c0f49..a9aef723164 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -5,12 +5,12 @@ import aiohomekit from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -import asynctest -from asynctest import patch import pytest from homeassistant.components.homekit_controller import config_flow +import tests.async_mock +from tests.async_mock import patch from tests.common import MockConfigEntry PAIRING_START_FORM_ERRORS = [ @@ -63,15 +63,15 @@ def _setup_flow_handler(hass, pairing=None): flow.hass = hass flow.context = {} - finish_pairing = asynctest.CoroutineMock(return_value=pairing) + finish_pairing = tests.async_mock.AsyncMock(return_value=pairing) discovery = mock.Mock() discovery.device_id = "00:00:00:00:00:00" - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) + discovery.start_pairing = tests.async_mock.AsyncMock(return_value=finish_pairing) flow.controller = mock.Mock() flow.controller.pairings = {} - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( + flow.controller.find_ip_by_device_id = tests.async_mock.AsyncMock( return_value=discovery ) @@ -368,7 +368,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) + finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -408,7 +408,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) + finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b1933604fbe..3ada7e7de0a 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,5 +1,4 @@ """Initializer helpers for HomematicIP fake server.""" -from asynctest import CoroutineMock, MagicMock, Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -23,6 +22,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory +from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry @@ -37,8 +37,8 @@ def mock_connection_fixture() -> AsyncConnection: connection._restCall.side_effect = ( # pylint: disable=protected-access _rest_call_side_effect ) - connection.api_call = CoroutineMock(return_value=True) - connection.init = CoroutineMock(side_effect=True) + connection.api_call = AsyncMock(return_value=True) + connection.init = AsyncMock(side_effect=True) return connection diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 403dbd873be..fede095e57d 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,7 +1,6 @@ """Helper for HomematicIP Cloud Tests.""" import json -from asynctest import Mock, patch from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, @@ -22,6 +21,7 @@ from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import load_fixture HAPID = "3014F7110000000000000001" diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index e6e145fefba..e9ecab2dbfb 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for HomematicIP Cloud config flow.""" -from asynctest import patch - from homeassistant.components.homematicip_cloud.const import ( DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, @@ -9,6 +7,7 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_PIN, ) +from tests.async_mock import patch from tests.common import MockConfigEntry DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 71efac3a7c9..8a8d52d167a 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,5 +1,4 @@ """Common tests for HomematicIP devices.""" -from asynctest import patch from homematicip.base.enums import EventType from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN @@ -14,6 +13,8 @@ from .helper import ( get_and_check_entity_basics, ) +from tests.async_mock import patch + async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): """Ensure that all supported devices could be loaded.""" diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index e6e143973f3..ca701622e90 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,6 +1,5 @@ """Test HomematicIP Cloud accesspoint.""" -from asynctest import Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.base.base_connection import HmipConnectionError import pytest @@ -22,6 +21,8 @@ from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN +from tests.async_mock import Mock, patch + async def test_auth_setup(hass): """Test auth setup for client registration.""" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 8f2753bc499..5b201da8aa8 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,6 +1,5 @@ """Test HomematicIP Cloud setup process.""" -from asynctest import CoroutineMock, Mock, patch from homematicip.base.base_connection import HmipConnectionError from homeassistant.components.homematicip_cloud.const import ( @@ -21,6 +20,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -139,12 +139,12 @@ async def test_unload_entry(hass): with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup = CoroutineMock(return_value=True) + instance.async_setup = AsyncMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" - instance.async_reset = CoroutineMock(return_value=True) + instance.async_reset = AsyncMock(return_value=True) assert await async_setup_component(hass, HMIPC_DOMAIN, {}) @@ -181,12 +181,12 @@ async def test_setup_services_and_unload_services(hass): with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup = CoroutineMock(return_value=True) + instance.async_setup = AsyncMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" - instance.async_reset = CoroutineMock(return_value=True) + instance.async_reset = AsyncMock(return_value=True) assert await async_setup_component(hass, HMIPC_DOMAIN, {}) @@ -214,12 +214,12 @@ async def test_setup_two_haps_unload_one_by_one(hass): with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup = CoroutineMock(return_value=True) + instance.async_setup = AsyncMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" - instance.async_reset = CoroutineMock(return_value=True) + instance.async_reset = AsyncMock(return_value=True) assert await async_setup_component(hass, HMIPC_DOMAIN, {}) diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 4a62eb76c27..d3faa761435 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,6 +1,5 @@ """Test HTML5 notify platform.""" import json -from unittest.mock import MagicMock, mock_open, patch from aiohttp.hdrs import AUTHORIZATION @@ -9,6 +8,8 @@ from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, mock_open, patch + CONFIG_FILE = "file.conf" VAPID_CONF = { diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 408bfd325c1..9282bf4587b 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,7 +1,6 @@ """The tests for the Home Assistant HTTP component.""" from datetime import timedelta from ipaddress import ip_network -from unittest.mock import patch from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized @@ -15,6 +14,8 @@ from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH, mock_real_ip +from tests.async_mock import patch + API_PASSWORD = "test-password" # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index ddf08de42b4..702912dd9d0 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -2,12 +2,10 @@ # pylint: disable=protected-access from ipaddress import ip_address import os -from unittest.mock import Mock, mock_open from aiohttp import web from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_middlewares import middleware -from asynctest import patch import pytest import homeassistant.components.http as http @@ -25,6 +23,8 @@ from homeassistant.setup import async_setup_component from . import mock_real_ip +from tests.async_mock import Mock, mock_open, patch + SUPERVISOR_IP = "1.2.3.4" BANNED_IPS = ["200.201.202.203", "100.64.0.2"] BANNED_IPS_WITH_SUPERVISOR = BANNED_IPS + [SUPERVISOR_IP] diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 04447191fd5..191cdb0ba49 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -1,6 +1,5 @@ """Test cors for the HTTP component.""" from pathlib import Path -from unittest.mock import patch from aiohttp import web from aiohttp.hdrs import ( @@ -19,6 +18,8 @@ from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH +from tests.async_mock import patch + TRUSTED_ORIGIN = "https://home-assistant.io" diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index 71b4b95cd8e..b0a14a31bc5 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -1,12 +1,12 @@ """Test data validator decorator.""" -from unittest.mock import Mock - from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator +from tests.async_mock import Mock + async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 58e6d8824dd..18ec9ccf471 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -2,12 +2,15 @@ from ipaddress import ip_network import logging import unittest -from unittest.mock import patch + +import pytest import homeassistant.components.http as http from homeassistant.setup import async_setup_component from homeassistant.util.ssl import server_context_intermediate, server_context_modern +from tests.async_mock import Mock, patch + class TestView(http.HomeAssistantView): """Test the HTTP views.""" @@ -38,49 +41,54 @@ class TestApiConfig(unittest.TestCase): def test_api_base_url_with_domain(hass): """Test setting API URL with domain.""" - api_config = http.ApiConfig("example.com") + api_config = http.ApiConfig("127.0.0.1", "example.com") assert api_config.base_url == "http://example.com:8123" def test_api_base_url_with_ip(hass): """Test setting API URL with IP.""" - api_config = http.ApiConfig("1.1.1.1") + api_config = http.ApiConfig("127.0.0.1", "1.1.1.1") assert api_config.base_url == "http://1.1.1.1:8123" def test_api_base_url_with_ip_and_port(hass): """Test setting API URL with IP and port.""" - api_config = http.ApiConfig("1.1.1.1", 8124) + api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", 8124) assert api_config.base_url == "http://1.1.1.1:8124" def test_api_base_url_with_protocol(hass): """Test setting API URL with protocol.""" - api_config = http.ApiConfig("https://example.com") + api_config = http.ApiConfig("127.0.0.1", "https://example.com") assert api_config.base_url == "https://example.com:8123" def test_api_base_url_with_protocol_and_port(hass): """Test setting API URL with protocol and port.""" - api_config = http.ApiConfig("https://example.com", 433) + api_config = http.ApiConfig("127.0.0.1", "https://example.com", 433) assert api_config.base_url == "https://example.com:433" def test_api_base_url_with_ssl_enable(hass): """Test setting API URL with use_ssl enabled.""" - api_config = http.ApiConfig("example.com", use_ssl=True) + api_config = http.ApiConfig("127.0.0.1", "example.com", use_ssl=True) assert api_config.base_url == "https://example.com:8123" def test_api_base_url_with_ssl_enable_and_port(hass): """Test setting API URL with use_ssl enabled and port.""" - api_config = http.ApiConfig("1.1.1.1", use_ssl=True, port=8888) + api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", use_ssl=True, port=8888) assert api_config.base_url == "https://1.1.1.1:8888" def test_api_base_url_with_protocol_and_ssl_enable(hass): """Test setting API URL with specific protocol and use_ssl enabled.""" - api_config = http.ApiConfig("http://example.com", use_ssl=True) + api_config = http.ApiConfig("127.0.0.1", "http://example.com", use_ssl=True) assert api_config.base_url == "http://example.com:8123" def test_api_base_url_removes_trailing_slash(hass): """Test a trialing slash is removed when setting the API URL.""" - api_config = http.ApiConfig("http://example.com/") + api_config = http.ApiConfig("127.0.0.1", "http://example.com/") assert api_config.base_url == "http://example.com:8123" + def test_api_local_ip(hass): + """Test a trialing slash is removed when setting the API URL.""" + api_config = http.ApiConfig("127.0.0.1", "http://example.com/") + assert api_config.local_ip == "127.0.0.1" + async def test_api_base_url_with_domain(hass): """Test setting API URL.""" @@ -116,6 +124,13 @@ async def test_api_no_base_url(hass): assert hass.config.api.base_url == "http://127.0.0.1:8123" +async def test_api_local_ip(hass): + """Test setting api url.""" + result = await async_setup_component(hass, "http", {"http": {}}) + assert result + assert hass.config.api.local_ip == "127.0.0.1" + + async def test_api_base_url_removes_trailing_slash(hass): """Test setting api url.""" result = await async_setup_component( @@ -258,3 +273,127 @@ async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) assert restored == http.HTTP_SCHEMA(config) + + +async def test_use_of_base_url(hass): + """Test detection base_url usage when called without integration context.""" + await async_setup_component(hass, "http", {"http": {}}) + with patch( + "homeassistant.components.http.extract_stack", + return_value=[ + Mock( + filename="/home/frenck/homeassistant/core.py", + lineno="21", + line="do_something()", + ), + Mock( + filename="/home/frenck/homeassistant/core.py", + lineno="42", + line="url = hass.config.api.base_url", + ), + Mock( + filename="/home/frenck/example/client.py", + lineno="21", + line="something()", + ), + ], + ), pytest.raises(RuntimeError): + hass.config.api.base_url + + +async def test_use_of_base_url_integration(hass, caplog): + """Test detection base_url usage when called with integration context.""" + await async_setup_component(hass, "http", {"http": {}}) + with patch( + "homeassistant.components.http.extract_stack", + return_value=[ + Mock( + filename="/home/frenck/homeassistant/core.py", + lineno="21", + line="do_something()", + ), + Mock( + filename="/home/frenck/homeassistant/components/example/__init__.py", + lineno="42", + line="url = hass.config.api.base_url", + ), + Mock( + filename="/home/frenck/example/client.py", + lineno="21", + line="something()", + ), + ], + ): + assert hass.config.api.base_url == "http://127.0.0.1:8123" + + assert ( + "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue for example using this method at homeassistant/components/example/__init__.py, line 42: url = hass.config.api.base_url" + in caplog.text + ) + + +async def test_use_of_base_url_integration_webhook(hass, caplog): + """Test detection base_url usage when called with integration context.""" + await async_setup_component(hass, "http", {"http": {}}) + with patch( + "homeassistant.components.http.extract_stack", + return_value=[ + Mock( + filename="/home/frenck/homeassistant/core.py", + lineno="21", + line="do_something()", + ), + Mock( + filename="/home/frenck/homeassistant/components/example/__init__.py", + lineno="42", + line="url = hass.config.api.base_url", + ), + Mock( + filename="/home/frenck/homeassistant/components/webhook/__init__.py", + lineno="42", + line="return get_url(hass)", + ), + Mock( + filename="/home/frenck/example/client.py", + lineno="21", + line="something()", + ), + ], + ): + assert hass.config.api.base_url == "http://127.0.0.1:8123" + + assert ( + "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue for example using this method at homeassistant/components/example/__init__.py, line 42: url = hass.config.api.base_url" + in caplog.text + ) + + +async def test_use_of_base_url_custom_component(hass, caplog): + """Test detection base_url usage when called with custom component context.""" + await async_setup_component(hass, "http", {"http": {}}) + with patch( + "homeassistant.components.http.extract_stack", + return_value=[ + Mock( + filename="/home/frenck/homeassistant/core.py", + lineno="21", + line="do_something()", + ), + Mock( + filename="/home/frenck/.homeassistant/custom_components/example/__init__.py", + lineno="42", + line="url = hass.config.api.base_url", + ), + Mock( + filename="/home/frenck/example/client.py", + lineno="21", + line="something()", + ), + ], + ): + assert hass.config.api.base_url == "http://127.0.0.1:8123" + + assert ( + "Detected use of deprecated `base_url` property, use `homeassistant.helpers.network.get_url` method instead. Please report issue to the custom component author for example using this method at custom_components/example/__init__.py, line 42: url = hass.config.api.base_url" + in caplog.text + ) diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 414ad4e8cb0..a6e4bdc12c8 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,6 +1,4 @@ """Tests for Home Assistant View.""" -from unittest.mock import Mock - from aiohttp.web_exceptions import ( HTTPBadRequest, HTTPInternalServerError, @@ -15,7 +13,7 @@ from homeassistant.components.http.view import ( ) from homeassistant.exceptions import ServiceNotFound, Unauthorized -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock, Mock @pytest.fixture @@ -38,7 +36,7 @@ async def test_handling_unauthorized(mock_request): """Test handling unauth exceptions.""" with pytest.raises(HTTPUnauthorized): await request_handler_factory( - Mock(requires_auth=False), mock_coro_func(exception=Unauthorized) + Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized) )(mock_request) @@ -46,7 +44,7 @@ async def test_handling_invalid_data(mock_request): """Test handling unauth exceptions.""" with pytest.raises(HTTPBadRequest): await request_handler_factory( - Mock(requires_auth=False), mock_coro_func(exception=vol.Invalid("yo")) + Mock(requires_auth=False), AsyncMock(side_effect=vol.Invalid("yo")) )(mock_request) @@ -55,5 +53,5 @@ async def test_handling_service_not_found(mock_request): with pytest.raises(HTTPInternalServerError): await request_handler_factory( Mock(requires_auth=False), - mock_coro_func(exception=ServiceNotFound("test", "test")), + AsyncMock(side_effect=ServiceNotFound("test", "test")), )(mock_request) diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 86de1ad8bd1..ae1e8184727 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -138,7 +138,7 @@ async def test_success(hass, login_requests_mock): login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", - text=f"OK", + text="OK", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index fa7c4ac473d..52ae43d65b4 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,6 +1,5 @@ """Test helpers for Hue.""" from collections import deque -from unittest.mock import Mock, patch from aiohue.groups import Groups from aiohue.lights import Lights @@ -11,6 +10,8 @@ from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base +from tests.async_mock import Mock, patch + @pytest.fixture(autouse=True) def no_request_delay(): diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index c122caf3760..385097514f8 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,18 +1,16 @@ """Test Hue bridge.""" -from unittest.mock import Mock, patch - import pytest from homeassistant.components.hue import bridge, errors from homeassistant.exceptions import ConfigEntryNotReady -from tests.common import mock_coro +from tests.async_mock import AsyncMock, Mock, patch async def test_bridge_setup(hass): """Test a successful setup.""" entry = Mock() - api = Mock(initialize=mock_coro) + api = Mock(initialize=AsyncMock()) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) @@ -35,9 +33,7 @@ async def test_bridge_setup_invalid_username(hass): with patch.object( bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired - ), patch.object( - hass.config_entries.flow, "async_init", return_value=mock_coro() - ) as mock_init: + ), patch.object(hass.config_entries.flow, "async_init") as mock_init: assert await hue_bridge.async_setup() is False assert len(mock_init.mock_calls) == 1 @@ -78,18 +74,16 @@ async def test_reset_unloads_entry_if_setup(hass): entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object( - bridge, "authenticate_bridge", return_value=mock_coro(Mock()) - ), patch("aiohue.Bridge", return_value=Mock()), patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward: + with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( + "aiohue.Bridge", return_value=Mock() + ), patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: assert await hue_bridge.async_setup() is True assert len(hass.services.async_services()) == 1 assert len(mock_forward.mock_calls) == 3 with patch.object( - hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + hass.config_entries, "async_forward_entry_unload", return_value=True ) as mock_forward: assert await hue_bridge.async_reset() @@ -99,13 +93,13 @@ async def test_reset_unloads_entry_if_setup(hass): async def test_handle_unauthorized(hass): """Test handling an unauthorized error on update.""" - entry = Mock(async_setup=Mock(return_value=mock_coro(Mock()))) + entry = Mock(async_setup=AsyncMock()) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object( - bridge, "authenticate_bridge", return_value=mock_coro(Mock()) - ), patch("aiohue.Bridge", return_value=Mock()): + with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( + "aiohue.Bridge", return_value=Mock() + ): assert await hue_bridge.async_setup() is True assert hue_bridge.authorized is True diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 87d4dc2b887..4ba4ecb06a6 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,11 +1,9 @@ """Tests for Philips Hue config flow.""" import asyncio -from unittest.mock import Mock from aiohttp import client_exceptions import aiohue from aiohue.discovery import URL_NUPNP -from asynctest import CoroutineMock, patch import pytest import voluptuous as vol @@ -13,6 +11,7 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -41,7 +40,7 @@ def get_mock_bridge( mock_create_user = create_user mock_bridge.create_user = mock_create_user - mock_bridge.initialize = CoroutineMock() + mock_bridge.initialize = AsyncMock() return mock_bridge @@ -190,7 +189,7 @@ async def test_flow_timeout_discovery(hass): async def test_flow_link_timeout(hass): """Test config flow.""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=asyncio.TimeoutError), + mock_create_user=AsyncMock(side_effect=asyncio.TimeoutError), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", @@ -211,7 +210,7 @@ async def test_flow_link_timeout(hass): async def test_flow_link_unknown_error(hass): """Test if a unknown error happened during the linking processes.""" - mock_bridge = get_mock_bridge(mock_create_user=CoroutineMock(side_effect=OSError),) + mock_bridge = get_mock_bridge(mock_create_user=AsyncMock(side_effect=OSError),) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", return_value=[mock_bridge], @@ -232,7 +231,7 @@ async def test_flow_link_unknown_error(hass): async def test_flow_link_button_not_pressed(hass): """Test config flow .""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=aiohue.LinkButtonNotPressed), + mock_create_user=AsyncMock(side_effect=aiohue.LinkButtonNotPressed), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", @@ -254,7 +253,7 @@ async def test_flow_link_button_not_pressed(hass): async def test_flow_link_unknown_host(hass): """Test config flow .""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=client_exceptions.ClientOSError), + mock_create_user=AsyncMock(side_effect=client_exceptions.ClientOSError), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 51ea3f2ae71..a144902bbc8 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,11 +1,10 @@ """Test Hue setup process.""" from unittest.mock import Mock -from asynctest import CoroutineMock, patch - from homeassistant.components import hue from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry, mock_coro @@ -102,9 +101,9 @@ async def test_config_passed_to_config_entry(hass): mock_registry = Mock() with patch.object(hue, "HueBridge") as mock_bridge, patch( "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_coro(mock_registry), + return_value=mock_registry, ): - mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.async_setup = AsyncMock(return_value=True) mock_bridge.return_value.api.config = Mock( mac="mock-mac", bridgeid="mock-bridgeid", @@ -159,13 +158,13 @@ async def test_unload_entry(hass): "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_coro(Mock()), ): - mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.async_setup = AsyncMock(return_value=True) mock_bridge.return_value.api.config = Mock(bridgeid="aabbccddeeff") assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge.return_value.mock_calls) == 1 - mock_bridge.return_value.async_reset.return_value = mock_coro(True) + mock_bridge.return_value.async_reset = AsyncMock(return_value=True) assert await hue.async_unload_entry(hass, entry) assert len(mock_bridge.return_value.async_reset.mock_calls) == 1 assert hass.data[hue.DOMAIN] == {} @@ -180,7 +179,7 @@ async def test_setting_unique_id(hass): "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_coro(Mock()), ): - mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.async_setup = AsyncMock(return_value=True) mock_bridge.return_value.api.config = Mock(bridgeid="mock-id") assert await async_setup_component(hass, hue.DOMAIN, {}) is True @@ -201,7 +200,7 @@ async def test_security_vuln_check(hass): "HueBridge", Mock( return_value=Mock( - async_setup=CoroutineMock(return_value=True), api=Mock(config=config) + async_setup=AsyncMock(return_value=True), api=Mock(config=config) ) ), ): diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 85ef0052029..630e74cb1af 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -1,7 +1,6 @@ """Philips Hue lights platform tests.""" import asyncio import logging -from unittest.mock import Mock import aiohue @@ -10,6 +9,8 @@ from homeassistant.components import hue from homeassistant.components.hue import light as hue_light from homeassistant.util import color +from tests.async_mock import Mock + _LOGGER = logging.getLogger(__name__) HUE_LIGHT_NS = "homeassistant.components.light.hue." diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 576bc365d50..e50ac71c03a 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,7 +1,6 @@ """Philips Hue sensors platform tests.""" import asyncio import logging -from unittest.mock import Mock import aiohue @@ -9,6 +8,8 @@ from homeassistant.components.hue.hue_event import CONF_HUE_EVENT from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge +from tests.async_mock import Mock + _LOGGER = logging.getLogger(__name__) PRESENCE_SENSOR_1_PRESENT = { diff --git a/tests/components/hunterdouglas_powerview/__init__.py b/tests/components/hunterdouglas_powerview/__init__.py new file mode 100644 index 00000000000..034d845b110 --- /dev/null +++ b/tests/components/hunterdouglas_powerview/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hunter Douglas PowerView integration.""" diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py new file mode 100644 index 00000000000..383d0445a7d --- /dev/null +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -0,0 +1,220 @@ +"""Test the Logitech Harmony Hub config flow.""" +import asyncio +import json + +from homeassistant import config_entries, setup +from homeassistant.components.hunterdouglas_powerview.const import DOMAIN + +from tests.async_mock import AsyncMock, MagicMock, patch +from tests.common import MockConfigEntry, load_fixture + + +def _get_mock_powerview_userdata(userdata=None, get_resources=None): + mock_powerview_userdata = MagicMock() + if not userdata: + userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata.json")) + if get_resources: + type(mock_powerview_userdata).get_resources = AsyncMock( + side_effect=get_resources + ) + else: + type(mock_powerview_userdata).get_resources = AsyncMock(return_value=userdata) + return mock_powerview_userdata + + +async def test_user_form(hass): + """Test we get the user form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_powerview_userdata = _get_mock_powerview_userdata() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ), patch( + "homeassistant.components.hunterdouglas_powerview.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.hunterdouglas_powerview.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "AlexanderHD" + assert result2["data"] == { + "host": "1.2.3.4", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result3["type"] == "form" + assert result3["errors"] == {} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], {"host": "1.2.3.4"}, + ) + assert result4["type"] == "abort" + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_powerview_userdata = _get_mock_powerview_userdata() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ), patch( + "homeassistant.components.hunterdouglas_powerview.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.hunterdouglas_powerview.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": "1.2.3.4"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "AlexanderHD" + assert result["data"] == { + "host": "1.2.3.4", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_homekit(hass): + """Test we get the form with homekit source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + ignored_config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + ignored_config_entry.add_to_hass(hass) + + mock_powerview_userdata = _get_mock_powerview_userdata() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "homekit"}, + data={ + "host": "1.2.3.4", + "properties": {"id": "AA::BB::CC::DD::EE::FF"}, + "name": "PowerViewHub._hap._tcp.local.", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] is None + assert result["description_placeholders"] == { + "host": "1.2.3.4", + "name": "PowerViewHub", + } + + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ), patch( + "homeassistant.components.hunterdouglas_powerview.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.hunterdouglas_powerview.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == "create_entry" + assert result2["title"] == "PowerViewHub" + assert result2["data"] == {"host": "1.2.3.4"} + assert result2["result"].unique_id == "ABC123" + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "homekit"}, + data={ + "host": "1.2.3.4", + "properties": {"id": "AA::BB::CC::DD::EE::FF"}, + "name": "PowerViewHub._hap._tcp.local.", + }, + ) + assert result3["type"] == "abort" + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerview_userdata = _get_mock_powerview_userdata( + get_resources=asyncio.TimeoutError + ) + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_no_data(hass): + """Test we handle no data being returned from the hub.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerview_userdata = _get_mock_powerview_userdata(userdata={"userData": {}}) + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_unknown_exception(hass): + """Test we handle unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerview_userdata = _get_mock_powerview_userdata(userdata={"userData": {}}) + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index 2230cc2ea32..2ed9006cdb6 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -1,8 +1,8 @@ """Configure iCloud tests.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + @pytest.fixture(name="icloud_bypass_setup", autouse=True) def icloud_bypass_setup_fixture(): diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 4bce35d0a63..3123ead4eeb 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the iCloud config flow.""" -from unittest.mock import MagicMock, Mock, patch - from pyicloud.exceptions import PyiCloudFailedLoginException import pytest @@ -22,6 +20,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry USERNAME = "username@me.com" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index d10df2492d4..e5fd8841a2b 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,10 +1,10 @@ """Test the init file of IFTTT.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components import ifttt from homeassistant.core import callback +from tests.async_mock import patch + async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up IFTTT and sending webhook.""" diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index c2bd357ecc8..8b9852ea234 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the IGN Sismologia (Earthquakes) Feed platform.""" import datetime -from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -29,6 +28,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "ign_sismologia", CONF_RADIUS: 200}]} diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 399523dd779..cc708db75db 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,8 +1,4 @@ """The tests for the image_processing component.""" -from unittest.mock import PropertyMock - -from asynctest import patch - import homeassistant.components.http as http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE @@ -10,6 +6,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component +from tests.async_mock import PropertyMock, patch from tests.common import ( assert_setup_component, get_test_home_assistant, diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index c0bdad3eacd..1e3150e687a 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,7 +1,6 @@ """The tests for the input_boolean component.""" # pylint: disable=protected-access import logging -from unittest.mock import patch import pytest @@ -23,6 +22,7 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import mock_component, mock_restore_cache _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index afee32702c4..0eb4d748563 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,7 +1,6 @@ """Tests for the Input slider component.""" # pylint: disable=protected-access import datetime -from unittest.mock import patch import pytest import voluptuous as vol @@ -27,6 +26,7 @@ from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import mock_restore_cache INITIAL_DATE = "2020-01-10" diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 28b9d27d23f..8971439de74 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,7 +1,4 @@ """The tests for the Input number component.""" -# pylint: disable=protected-access -from unittest.mock import patch - import pytest import voluptuous as vol @@ -24,6 +21,8 @@ from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +# pylint: disable=protected-access +from tests.async_mock import patch from tests.common import mock_restore_cache diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 5c470ca5bfc..6f83de340f3 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,7 +1,4 @@ """The tests for the Input select component.""" -# pylint: disable=protected-access -from unittest.mock import patch - import pytest from homeassistant.components.input_select import ( @@ -28,6 +25,8 @@ from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component +# pylint: disable=protected-access +from tests.async_mock import patch from tests.common import mock_restore_cache diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index cc226dc1d87..ed89ccd7087 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,7 +1,4 @@ """The tests for the Input text component.""" -# pylint: disable=protected-access -from unittest.mock import patch - import pytest from homeassistant.components.input_text import ( @@ -29,6 +26,8 @@ from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component +# pylint: disable=protected-access +from tests.async_mock import patch from tests.common import mock_restore_cache TEST_VAL_MIN = 2 diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 3afa5c14c22..c36a718ffb4 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -1,11 +1,12 @@ """The tests for the integration sensor platform.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch + async def test_state(hass): """Test integration sensor state.""" diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index fd44f8b2a58..b3b8968f451 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,10 +1,9 @@ """Tests for IPMA config flow.""" -from unittest.mock import Mock, patch from homeassistant.components.ipma import config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_show_config_form(): @@ -58,8 +57,8 @@ async def test_flow_show_form(): flow = config_flow.IpmaFlowHandler() flow.hass = hass - with patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch( + "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form: await flow.async_step_user() assert len(config_form.mock_calls) == 1 @@ -77,10 +76,10 @@ async def test_flow_entry_created_from_user_input(): test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} # Test that entry created when user_input name not exists - with patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch( + "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form, patch.object( - flow.hass.config_entries, "async_entries", return_value=mock_coro() + flow.hass.config_entries, "async_entries", return_value=[], ) as config_entries: result = await flow.async_step_user(user_input=test_data) @@ -104,8 +103,8 @@ async def test_flow_entry_config_entry_already_exists(): test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} # Test that entry created when user_input name not exists - with patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch( + "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form, patch.object( flow.hass.config_entries, "async_entries", return_value={"home": test_data} ) as config_entries: diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 7a6e1160f24..e7542070d2c 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,6 +1,5 @@ """The tests for the IPMA weather component.""" from collections import namedtuple -from unittest.mock import patch from homeassistant.components import weather from homeassistant.components.weather import ( @@ -22,7 +21,8 @@ from homeassistant.components.weather import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import now -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} @@ -128,7 +128,7 @@ async def test_setup_configuration(hass): """Test for successfully setting up the IPMA platform.""" with patch( "homeassistant.components.ipma.weather.async_get_location", - return_value=mock_coro(MockLocation()), + return_value=MockLocation(), ): assert await async_setup_component( hass, @@ -153,7 +153,7 @@ async def test_setup_config_flow(hass): """Test for successfully setting up the IPMA platform.""" with patch( "homeassistant.components.ipma.weather.async_get_location", - return_value=mock_coro(MockLocation()), + return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) @@ -175,7 +175,7 @@ async def test_daily_forecast(hass): """Test for successfully getting daily forecast.""" with patch( "homeassistant.components.ipma.weather.async_get_location", - return_value=mock_coro(MockLocation()), + return_value=MockLocation(), ): assert await async_setup_component( hass, @@ -201,7 +201,7 @@ async def test_hourly_forecast(hass): """Test for successfully getting daily forecast.""" with patch( "homeassistant.components.ipma.weather.async_get_location", - return_value=mock_coro(MockLocation()), + return_value=MockLocation(), ): assert await async_setup_component( hass, diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index f0dc45417e1..515543f3cf5 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -1,6 +1,9 @@ """Tests for the IPP integration.""" import os +import aiohttp +from pyipp import IPPConnectionUpgradeRequired, IPPError + from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.const import ( CONF_HOST, @@ -18,21 +21,25 @@ from tests.test_util.aiohttp import AiohttpClientMocker ATTR_HOSTNAME = "hostname" ATTR_PROPERTIES = "properties" +HOST = "192.168.1.31" +PORT = 631 +BASE_PATH = "/ipp/print" + IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local." IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local." ZEROCONF_NAME = "EPSON XP-6000 Series" -ZEROCONF_HOST = "192.168.1.31" +ZEROCONF_HOST = HOST ZEROCONF_HOSTNAME = "EPSON123456.local." -ZEROCONF_PORT = 631 - +ZEROCONF_PORT = PORT +ZEROCONF_RP = "ipp/print" MOCK_USER_INPUT = { - CONF_HOST: "192.168.1.31", - CONF_PORT: 361, + CONF_HOST: HOST, + CONF_PORT: PORT, CONF_SSL: False, CONF_VERIFY_SSL: False, - CONF_BASE_PATH: "/ipp/print", + CONF_BASE_PATH: BASE_PATH, } MOCK_ZEROCONF_IPP_SERVICE_INFO = { @@ -41,7 +48,7 @@ MOCK_ZEROCONF_IPP_SERVICE_INFO = { CONF_HOST: ZEROCONF_HOST, ATTR_HOSTNAME: ZEROCONF_HOSTNAME, CONF_PORT: ZEROCONF_PORT, - ATTR_PROPERTIES: {"rp": "ipp/print"}, + ATTR_PROPERTIES: {"rp": ZEROCONF_RP}, } MOCK_ZEROCONF_IPPS_SERVICE_INFO = { @@ -50,7 +57,7 @@ MOCK_ZEROCONF_IPPS_SERVICE_INFO = { CONF_HOST: ZEROCONF_HOST, ATTR_HOSTNAME: ZEROCONF_HOSTNAME, CONF_PORT: ZEROCONF_PORT, - ATTR_PROPERTIES: {"rp": "ipp/print"}, + ATTR_PROPERTIES: {"rp": ZEROCONF_RP}, } @@ -61,30 +68,75 @@ def load_fixture_binary(filename): return fptr.read() +def mock_connection( + aioclient_mock: AiohttpClientMocker, + host: str = HOST, + port: int = PORT, + ssl: bool = False, + base_path: str = BASE_PATH, + conn_error: bool = False, + conn_upgrade_error: bool = False, + ipp_error: bool = False, + no_unique_id: bool = False, + parse_error: bool = False, + version_not_supported: bool = False, +): + """Mock the IPP connection.""" + scheme = "https" if ssl else "http" + ipp_url = f"{scheme}://{host}:{port}" + + if ipp_error: + aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPError) + return + + if conn_error: + aioclient_mock.post(f"{ipp_url}{base_path}", exc=aiohttp.ClientError) + return + + if conn_upgrade_error: + aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPConnectionUpgradeRequired) + return + + fixture = "ipp/get-printer-attributes.bin" + if no_unique_id: + fixture = "ipp/get-printer-attributes-success-nodata.bin" + elif version_not_supported: + fixture = "ipp/get-printer-attributes-error-0x0503.bin" + + if parse_error: + content = "BAD" + else: + content = load_fixture_binary(fixture) + + aioclient_mock.post( + f"{ipp_url}{base_path}", + content=content, + headers={"Content-Type": "application/ipp"}, + ) + + async def init_integration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, + host: str = HOST, + port: int = PORT, + ssl: bool = False, + base_path: str = BASE_PATH, uuid: str = "cfe92100-67c4-11d4-a45f-f8d027761251", unique_id: str = "cfe92100-67c4-11d4-a45f-f8d027761251", + conn_error: bool = False, ) -> MockConfigEntry: """Set up the IPP integration in Home Assistant.""" - fixture = "ipp/get-printer-attributes.bin" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content=load_fixture_binary(fixture), - headers={"Content-Type": "application/ipp"}, - ) - entry = MockConfigEntry( domain=DOMAIN, unique_id=unique_id, data={ - CONF_HOST: "192.168.1.31", - CONF_PORT: 631, - CONF_SSL: False, + CONF_HOST: host, + CONF_PORT: port, + CONF_SSL: ssl, CONF_VERIFY_SSL: True, - CONF_BASE_PATH: "/ipp/print", + CONF_BASE_PATH: base_path, CONF_UUID: uuid, }, ) @@ -92,6 +144,14 @@ async def init_integration( entry.add_to_hass(hass) if not skip_setup: + mock_connection( + aioclient_mock, + host=host, + port=port, + ssl=ssl, + base_path=base_path, + conn_error=conn_error, + ) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index fb75ba9caef..0093ba57e5b 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,7 +1,4 @@ """Tests for the IPP config flow.""" -import aiohttp -from pyipp import IPPConnectionUpgradeRequired, IPPError - from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL @@ -17,7 +14,7 @@ from . import ( MOCK_ZEROCONF_IPP_SERVICE_INFO, MOCK_ZEROCONF_IPPS_SERVICE_INFO, init_integration, - load_fixture_binary, + mock_connection, ) from tests.test_util.aiohttp import AiohttpClientMocker @@ -37,11 +34,7 @@ async def test_show_zeroconf_form( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that the zeroconf confirmation form is served.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content=load_fixture_binary("ipp/get-printer-attributes.bin"), - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -57,7 +50,7 @@ async def test_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on IPP connection error.""" - aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) + mock_connection(aioclient_mock, conn_error=True) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -73,7 +66,7 @@ async def test_zeroconf_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) + mock_connection(aioclient_mock, conn_error=True) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -88,7 +81,7 @@ async def test_zeroconf_confirm_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) + mock_connection(aioclient_mock, conn_error=True) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -103,9 +96,7 @@ async def test_user_connection_upgrade_required( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show the user form if connection upgrade required by server.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired - ) + mock_connection(aioclient_mock, conn_upgrade_error=True) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -121,9 +112,7 @@ async def test_zeroconf_connection_upgrade_required( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired - ) + mock_connection(aioclient_mock, conn_upgrade_error=True) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -138,11 +127,7 @@ async def test_user_parse_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort user flow on IPP parse error.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content="BAD", - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock, parse_error=True) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -157,11 +142,7 @@ async def test_zeroconf_parse_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP parse error.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content="BAD", - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock, parse_error=True) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -176,7 +157,7 @@ async def test_user_ipp_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort the user flow on IPP error.""" - aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError) + mock_connection(aioclient_mock, ipp_error=True) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -191,7 +172,7 @@ async def test_zeroconf_ipp_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP error.""" - aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=IPPError) + mock_connection(aioclient_mock, ipp_error=True) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -206,11 +187,7 @@ async def test_user_ipp_version_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort user flow on IPP version not supported error.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"), - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock, version_not_supported=True) user_input = {**MOCK_USER_INPUT} result = await hass.config_entries.flow.async_init( @@ -225,11 +202,7 @@ async def test_zeroconf_ipp_version_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP version not supported error.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content=load_fixture_binary("ipp/get-printer-attributes-error-0x0503.bin"), - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock, version_not_supported=True) discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO} result = await hass.config_entries.flow.async_init( @@ -291,15 +264,26 @@ async def test_zeroconf_with_uuid_device_exists_abort( assert result["reason"] == "already_configured" +async def test_zeroconf_unique_id_required_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if printer lacks unique identification.""" + mock_connection(aioclient_mock, no_unique_id=True) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unique_id_required" + + async def test_full_user_flow_implementation( hass: HomeAssistant, aioclient_mock ) -> None: """Test the full manual user flow from start to finish.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content=load_fixture_binary("ipp/get-printer-attributes.bin"), - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -328,11 +312,7 @@ async def test_full_zeroconf_flow_implementation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" - aioclient_mock.post( - "http://192.168.1.31:631/ipp/print", - content=load_fixture_binary("ipp/get-printer-attributes.bin"), - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -363,11 +343,7 @@ async def test_full_zeroconf_tls_flow_implementation( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" - aioclient_mock.post( - "https://192.168.1.31:631/ipp/print", - content=load_fixture_binary("ipp/get-printer-attributes.bin"), - headers={"Content-Type": "application/ipp"}, - ) + mock_connection(aioclient_mock, ssl=True) discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index 2ec11a1e937..f06be4fc8b5 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -1,6 +1,4 @@ """Tests for the IPP integration.""" -import aiohttp - from homeassistant.components.ipp.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -17,9 +15,7 @@ async def test_config_entry_not_ready( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the IPP configuration entry not ready.""" - aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) - - entry = await init_integration(hass, aioclient_mock) + entry = await init_integration(hass, aioclient_mock, conn_error=True) assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index e6830f559c6..1abf557c93b 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,15 +1,14 @@ """Tests for the IPP sensor platform.""" from datetime import datetime -from asynctest import patch - from homeassistant.components.ipp.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, UNIT_PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from tests.components.ipp import init_integration +from tests.async_mock import patch +from tests.components.ipp import init_integration, mock_connection from tests.test_util.aiohttp import AiohttpClientMocker @@ -17,6 +16,8 @@ async def test_sensors( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the creation and values of the IPP sensors.""" + mock_connection(aioclient_mock) + entry = await init_integration(hass, aioclient_mock, skip_setup=True) registry = await hass.helpers.entity_registry.async_get_registry() diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 9345ff5b2ad..4cc30958b23 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,17 +1,8 @@ """Define tests for the IQVIA config flow.""" -import pytest - from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN, config_flow -from tests.common import MockConfigEntry, MockDependency - - -@pytest.fixture -def mock_pyiqvia(): - """Mock the pyiqvia library.""" - with MockDependency("pyiqvia") as mock_pyiqvia_: - yield mock_pyiqvia_ +from tests.common import MockConfigEntry async def test_duplicate_error(hass): @@ -26,7 +17,7 @@ async def test_duplicate_error(hass): assert result["errors"] == {CONF_ZIP_CODE: "identifier_exists"} -async def test_invalid_zip_code(hass, mock_pyiqvia): +async def test_invalid_zip_code(hass): """Test that an invalid ZIP code key throws an error.""" conf = {CONF_ZIP_CODE: "abcde"} @@ -48,7 +39,7 @@ async def test_show_form(hass): assert result["step_id"] == "user" -async def test_step_import(hass, mock_pyiqvia): +async def test_step_import(hass): """Test that the import step works.""" conf = {CONF_ZIP_CODE: "12345"} @@ -61,7 +52,7 @@ async def test_step_import(hass, mock_pyiqvia): assert result["data"] == {CONF_ZIP_CODE: "12345"} -async def test_step_user(hass, mock_pyiqvia): +async def test_step_user(hass): """Test that the user step works.""" conf = {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index a56178e5225..204075ecb8c 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for Islamic Prayer Times config flow.""" -from unittest.mock import patch - import pytest from homeassistant import data_entry_flow from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index e91e83b315e..9fb9333e045 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,7 +1,6 @@ """Tests for Islamic Prayer Times init.""" from datetime import timedelta -from unittest.mock import patch from prayer_times_calculator.exceptions import InvalidResponseError @@ -17,6 +16,7 @@ from . import ( PRAYER_TIMES_TIMESTAMPS, ) +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 5250732ad94..3ee6a59136a 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,11 +1,10 @@ """The tests for the Islamic prayer times sensor platform.""" -from unittest.mock import patch - from homeassistant.components import islamic_prayer_times import homeassistant.util.dt as dt_util from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TIMESTAMPS +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/isy994/__init__.py b/tests/components/isy994/__init__.py new file mode 100644 index 00000000000..9aee1e15905 --- /dev/null +++ b/tests/components/isy994/__init__.py @@ -0,0 +1 @@ +"""Tests for the Universal Devices ISY994 integration.""" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py new file mode 100644 index 00000000000..55311494d30 --- /dev/null +++ b/tests/components/isy994/test_config_flow.py @@ -0,0 +1,302 @@ +"""Test the Universal Devices ISY994 config flow.""" + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components import ssdp +from homeassistant.components.isy994.config_flow import CannotConnect +from homeassistant.components.isy994.const import ( + CONF_IGNORE_STRING, + CONF_RESTORE_LIGHT_STATE, + CONF_SENSOR_STRING, + CONF_TLS_VER, + CONF_VAR_SENSOR_STRING, + DOMAIN, + ISY_URL_POSTFIX, + UDN_UUID_PREFIX, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +MOCK_HOSTNAME = "1.1.1.1" +MOCK_USERNAME = "test-username" +MOCK_PASSWORD = "test-password" + +# Don't use the integration defaults here to make sure they're being set correctly. +MOCK_TLS_VERSION = 1.2 +MOCK_IGNORE_STRING = "{IGNOREME}" +MOCK_RESTORE_LIGHT_STATE = True +MOCK_SENSOR_STRING = "IMASENSOR" +MOCK_VARIABLE_SENSOR_STRING = "HomeAssistant." + +MOCK_USER_INPUT = { + CONF_HOST: f"http://{MOCK_HOSTNAME}", + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_TLS_VER: MOCK_TLS_VERSION, +} +MOCK_IMPORT_WITH_SSL = { + CONF_HOST: f"https://{MOCK_HOSTNAME}", + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_TLS_VER: MOCK_TLS_VERSION, +} +MOCK_IMPORT_BASIC_CONFIG = { + CONF_HOST: f"http://{MOCK_HOSTNAME}", + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, +} +MOCK_IMPORT_FULL_CONFIG = { + CONF_HOST: f"http://{MOCK_HOSTNAME}", + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_IGNORE_STRING: MOCK_IGNORE_STRING, + CONF_RESTORE_LIGHT_STATE: MOCK_RESTORE_LIGHT_STATE, + CONF_SENSOR_STRING: MOCK_SENSOR_STRING, + CONF_TLS_VER: MOCK_TLS_VERSION, + CONF_VAR_SENSOR_STRING: MOCK_VARIABLE_SENSOR_STRING, +} + +MOCK_DEVICE_NAME = "Name of the device" +MOCK_UUID = "CE:FB:72:31:B7:B9" +MOCK_VALIDATED_RESPONSE = {"name": MOCK_DEVICE_NAME, "uuid": MOCK_UUID} + +PATCH_CONFIGURATION = "homeassistant.components.isy994.config_flow.Configuration" +PATCH_CONNECTION = "homeassistant.components.isy994.config_flow.Connection" +PATCH_ASYNC_SETUP = "homeassistant.components.isy994.async_setup" +PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.isy994.async_setup_entry" + + +async def test_form(hass: HomeAssistantType): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class, patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, return_value=True, + ) as mock_setup_entry: + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USER_INPUT, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" + assert result2["result"].unique_id == MOCK_UUID + assert result2["data"] == MOCK_USER_INPUT + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_host(hass: HomeAssistantType): + """Test we handle invalid host.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": MOCK_HOSTNAME, # Test with missing protocol (http://) + "username": MOCK_USERNAME, + "password": MOCK_PASSWORD, + "tls": MOCK_TLS_VERSION, + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_host"} + + +async def test_form_invalid_auth(hass: HomeAssistantType): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch(PATCH_CONFIGURATION), patch( + PATCH_CONNECTION, side_effect=ValueError("PyISY could not connect to the ISY."), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistantType): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch(PATCH_CONFIGURATION), patch( + PATCH_CONNECTION, side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_existing_config_entry(hass: HomeAssistantType): + """Test if config entry already exists.""" + MockConfigEntry(domain=DOMAIN, unique_id=MOCK_UUID).add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class: + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USER_INPUT, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_import_flow_some_fields(hass: HomeAssistantType) -> None: + """Test import config flow with just the basic fields.""" + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + PATCH_ASYNC_SETUP_ENTRY, return_value=True, + ): + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_BASIC_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" + assert result["data"][CONF_USERNAME] == MOCK_USERNAME + assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD + + +async def test_import_flow_with_https(hass: HomeAssistantType) -> None: + """Test import config with https.""" + + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + PATCH_ASYNC_SETUP_ENTRY, return_value=True, + ): + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_WITH_SSL, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == f"https://{MOCK_HOSTNAME}" + assert result["data"][CONF_USERNAME] == MOCK_USERNAME + assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD + + +async def test_import_flow_all_fields(hass: HomeAssistantType) -> None: + """Test import config flow with all fields.""" + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + PATCH_ASYNC_SETUP_ENTRY, return_value=True, + ): + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_FULL_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" + assert result["data"][CONF_USERNAME] == MOCK_USERNAME + assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD + assert result["data"][CONF_IGNORE_STRING] == MOCK_IGNORE_STRING + assert result["data"][CONF_RESTORE_LIGHT_STATE] == MOCK_RESTORE_LIGHT_STATE + assert result["data"][CONF_SENSOR_STRING] == MOCK_SENSOR_STRING + assert result["data"][CONF_VAR_SENSOR_STRING] == MOCK_VARIABLE_SENSOR_STRING + assert result["data"][CONF_TLS_VER] == MOCK_TLS_VERSION + + +async def test_form_ssdp_already_configured(hass: HomeAssistantType) -> None: + """Test ssdp abort when the serial number is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}"}, + unique_id=MOCK_UUID, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_form_ssdp(hass: HomeAssistantType): + """Test we can setup from ssdp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", + ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch(PATCH_CONFIGURATION) as mock_config_class, patch( + PATCH_CONNECTION + ) as mock_connection_class, patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, return_value=True, + ) as mock_setup_entry: + isy_conn = mock_connection_class.return_value + isy_conn.get_config.return_value = None + mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" + assert result2["result"].unique_id == MOCK_UUID + assert result2["data"] == MOCK_USER_INPUT + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 5deafeb08a7..72b80e75bcf 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -1,13 +1,11 @@ """Tests for iZone.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE -from tests.common import mock_coro +from tests.async_mock import Mock, patch @pytest.fixture @@ -24,7 +22,7 @@ def _mock_start_discovery(hass, mock_disco): def do_disovered(*args): async_dispatcher_send(hass, DISPATCH_CONTROLLER_DISCOVERED, True) - return mock_coro(mock_disco) + return mock_disco return do_disovered @@ -36,7 +34,7 @@ async def test_not_found(hass, mock_disco): "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( "homeassistant.components.izone.config_flow.async_stop_discovery_service", - return_value=mock_coro(), + return_value=None, ) as stop_disco: start_disco.side_effect = _mock_start_discovery(hass, mock_disco) result = await hass.config_entries.flow.async_init( @@ -59,13 +57,12 @@ async def test_found(hass, mock_disco): mock_disco.pi_disco.controllers["blah"] = object() with patch( - "homeassistant.components.izone.climate.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.izone.climate.async_setup_entry", return_value=True, ) as mock_setup, patch( "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( "homeassistant.components.izone.async_start_discovery_service", - return_value=mock_coro(), + return_value=None, ): start_disco.side_effect = _mock_start_discovery(hass, mock_disco) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 592086461d3..8e18579b197 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -2,11 +2,12 @@ from collections import namedtuple from contextlib import contextmanager from datetime import datetime -from unittest.mock import patch from homeassistant.components import jewish_calendar import homeassistant.util.dt as dt_util +from tests.async_mock import patch + _LatLng = namedtuple("_LatLng", ["lat", "lng"]) NYC_LATLNG = _LatLng(40.7128, -74.0060) diff --git a/tests/components/juicenet/__init__.py b/tests/components/juicenet/__init__.py new file mode 100644 index 00000000000..cc125664bac --- /dev/null +++ b/tests/components/juicenet/__init__.py @@ -0,0 +1 @@ +"""Tests for the JuiceNet component.""" diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py new file mode 100644 index 00000000000..12edeee50c5 --- /dev/null +++ b/tests/components/juicenet/test_config_flow.py @@ -0,0 +1,123 @@ +"""Test the JuiceNet config flow.""" +import aiohttp +from asynctest import patch +from asynctest.mock import MagicMock +from pyjuicenet import TokenError + +from homeassistant import config_entries, setup +from homeassistant.components.juicenet.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN + + +def _mock_juicenet_return_value(get_devices=None): + juicenet_mock = MagicMock() + type(juicenet_mock).get_devices = MagicMock(return_value=get_devices) + return juicenet_mock + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "JuiceNet" + assert result2["data"] == {CONF_ACCESS_TOKEN: "access_token"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=TokenError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=aiohttp.ClientError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_catch_unknown_errors(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_import(hass): + """Test that import works as expected.""" + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ACCESS_TOKEN: "access_token"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "JuiceNet" + assert result["data"] == {CONF_ACCESS_TOKEN: "access_token"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index e5056235127..8656ac23264 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -4,11 +4,11 @@ import os import shutil import tempfile import unittest -from unittest.mock import MagicMock, patch import homeassistant.components.kira as kira from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant TEST_CONFIG = { diff --git a/tests/components/kira/test_remote.py b/tests/components/kira/test_remote.py index d7718ad33f0..b1ac7ea12fc 100644 --- a/tests/components/kira/test_remote.py +++ b/tests/components/kira/test_remote.py @@ -1,9 +1,9 @@ """The tests for Kira sensor platform.""" import unittest -from unittest.mock import MagicMock from homeassistant.components.kira import remote as kira +from tests.async_mock import MagicMock from tests.common import get_test_home_assistant SERVICE_SEND_COMMAND = "send_command" diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py index 55e6d5453c7..2fae36dc670 100644 --- a/tests/components/kira/test_sensor.py +++ b/tests/components/kira/test_sensor.py @@ -1,9 +1,9 @@ """The tests for Kira sensor platform.""" import unittest -from unittest.mock import MagicMock from homeassistant.components.kira import sensor as kira +from tests.async_mock import MagicMock from tests.common import get_test_home_assistant TEST_CONFIG = {kira.DOMAIN: {"sensors": [{"host": "127.0.0.1", "port": 17324}]}} diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 0bf6e7846ae..a0d870b37ff 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for Konnected Alarm Panel config flow.""" -from asynctest import patch import pytest from homeassistant.components import konnected from homeassistant.components.konnected import config_flow +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index f87c66fe412..74e3b931f61 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -1,12 +1,13 @@ """Test Konnected setup process.""" -from asynctest import patch import pytest from homeassistant.components import konnected from homeassistant.components.konnected import config_flow +from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry @@ -385,6 +386,9 @@ async def test_config_passed_to_config_entry(hass): async def test_unload_entry(hass, mock_panel): """Test being able to unload an entry.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) entry = MockConfigEntry( domain=konnected.DOMAIN, data={konnected.CONF_ID: "aabbccddeeff"} ) @@ -563,7 +567,9 @@ async def test_api(hass, aiohttp_client, mock_panel): async def test_state_updates_zone(hass, aiohttp_client, mock_panel): """Test callback view.""" - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) device_config = config_flow.CONFIG_ENTRY_SCHEMA( { @@ -711,7 +717,9 @@ async def test_state_updates_zone(hass, aiohttp_client, mock_panel): async def test_state_updates_pin(hass, aiohttp_client, mock_panel): """Test callback view.""" - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) device_config = config_flow.CONFIG_ENTRY_SCHEMA( { diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index f1ae8a4357c..f167c558d01 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -1,10 +1,10 @@ """Test Konnected setup process.""" -from asynctest import patch import pytest from homeassistant.components.konnected import config_flow, panel from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index c2ce8e947dd..7ae70a6f152 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -1,6 +1,4 @@ """Tests for the lastfm sensor.""" -from unittest.mock import patch - from pylast import Track import pytest @@ -8,6 +6,8 @@ from homeassistant.components import sensor from homeassistant.components.lastfm.sensor import STATE_NOT_SCROBBLING from homeassistant.setup import async_setup_component +from tests.async_mock import patch + class MockUser: """Mock user object for pylast.""" diff --git a/tests/components/light/common.py b/tests/components/light/common.py index aa1e62db5bf..a9991bf3594 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -128,16 +128,80 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): @bind_hass -def toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): +def toggle( + hass, + entity_id=ENTITY_MATCH_ALL, + transition=None, + brightness=None, + brightness_pct=None, + rgb_color=None, + xy_color=None, + hs_color=None, + color_temp=None, + kelvin=None, + white_value=None, + profile=None, + flash=None, + effect=None, + color_name=None, +): """Toggle all or specified light.""" - hass.add_job(async_toggle, hass, entity_id, transition) + hass.add_job( + async_toggle, + hass, + entity_id, + transition, + brightness, + brightness_pct, + rgb_color, + xy_color, + hs_color, + color_temp, + kelvin, + white_value, + profile, + flash, + effect, + color_name, + ) -async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): - """Toggle all or specified light.""" +async def async_toggle( + hass, + entity_id=ENTITY_MATCH_ALL, + transition=None, + brightness=None, + brightness_pct=None, + rgb_color=None, + xy_color=None, + hs_color=None, + color_temp=None, + kelvin=None, + white_value=None, + profile=None, + flash=None, + effect=None, + color_name=None, +): + """Turn all or specified light on.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_TRANSITION, transition)] + for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_PROFILE, profile), + (ATTR_TRANSITION, transition), + (ATTR_BRIGHTNESS, brightness), + (ATTR_BRIGHTNESS_PCT, brightness_pct), + (ATTR_RGB_COLOR, rgb_color), + (ATTR_XY_COLOR, xy_color), + (ATTR_HS_COLOR, hs_color), + (ATTR_COLOR_TEMP, color_temp), + (ATTR_KELVIN, kelvin), + (ATTR_WHITE_VALUE, white_value), + (ATTR_FLASH, flash), + (ATTR_EFFECT, effect), + (ATTR_COLOR_NAME, color_name), + ] if value is not None } diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 24645a32611..998ef7851c1 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,6 +1,5 @@ """The test for light device automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -11,6 +10,7 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 2e2f74828d9..b1f9327ff50 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -3,7 +3,6 @@ from io import StringIO import os import unittest -import unittest.mock as mock import pytest @@ -21,6 +20,7 @@ from homeassistant.const import ( from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component, setup_component +import tests.async_mock as mock from tests.common import get_test_home_assistant, mock_service, mock_storage from tests.components.light import common @@ -263,6 +263,15 @@ class TestLight(unittest.TestCase): light.ATTR_HS_COLOR: (prof_h, prof_s), } == data + # Test toggle with parameters + common.toggle(self.hass, ent3.entity_id, profile=prof_name, brightness_pct=100) + self.hass.block_till_done() + _, data = ent3.last_call("turn_on") + assert { + light.ATTR_BRIGHTNESS: 255, + light.ATTR_HS_COLOR: (prof_h, prof_s), + } == data + # Test bad data common.turn_on(self.hass) common.turn_on(self.hass, ent1.entity_id, profile="nonexisting") @@ -548,3 +557,13 @@ async def test_light_brightness_pct_conversion(hass): _, data = entity.last_call("turn_on") assert data["brightness"] == 255, data + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomLight(light.Light): + pass + + CustomLight() + assert "Light is deprecated, modify CustomLight" in caplog.text diff --git a/tests/components/linky/conftest.py b/tests/components/linky/conftest.py index f77f01a4ae7..93e3ff78d2b 100644 --- a/tests/components/linky/conftest.py +++ b/tests/components/linky/conftest.py @@ -1,8 +1,8 @@ """Linky generic test utils.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def patch_fakeuseragent(): diff --git a/tests/components/linky/test_config_flow.py b/tests/components/linky/test_config_flow.py index 8278a77d4d0..f39f0da7d99 100644 --- a/tests/components/linky/test_config_flow.py +++ b/tests/components/linky/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the Linky config flow.""" -from unittest.mock import Mock, patch - from pylinky.exceptions import ( PyLinkyAccessException, PyLinkyEnedisException, @@ -15,6 +13,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry USERNAME = "username@hotmail.fr" diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 63ef2e9f5f8..0f3d17c9b59 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,16 +1,17 @@ """The tests the for Locative device tracker platform.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components import locative from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE +from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component +from tests.async_mock import patch + # pylint: disable=redefined-outer-name @@ -33,7 +34,9 @@ async def locative_client(loop, hass, hass_client): @pytest.fixture async def webhook_id(hass, locative_client): """Initialize the Geofency component and get the webhook_id.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( "locative", context={"source": "user"} ) diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py new file mode 100644 index 00000000000..a788b9fa917 --- /dev/null +++ b/tests/components/lock/test_init.py @@ -0,0 +1,12 @@ +"""The tests for Lock.""" +from homeassistant.components import lock + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomLock(lock.LockDevice): + pass + + CustomLock() + assert "LockDevice is deprecated, modify CustomLock" in caplog.text diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 814e304f9f5..abe6f6ec515 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -5,7 +5,6 @@ from functools import partial import logging import unittest -from asynctest import patch import pytest import voluptuous as vol @@ -28,6 +27,7 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component from tests.components.recorder.common import trigger_db_commit diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 7ba3816b5e2..43dea15f7e6 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.logi_circle.config_flow import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock from tests.common import mock_coro @@ -51,8 +52,8 @@ def mock_logi_circle(): "homeassistant.components.logi_circle.config_flow.LogiCircle" ) as logi_circle: LogiCircle = logi_circle() - LogiCircle.authorize = Mock(return_value=mock_coro(return_value=True)) - LogiCircle.close = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.authorize = AsyncMock(return_value=True) + LogiCircle.close = AsyncMock(return_value=True) LogiCircle.account = mock_coro(return_value={"accountId": "testId"}) LogiCircle.authorize_url = "http://authorize.url" yield LogiCircle diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 8509ad37fcd..fb8c909eac5 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,12 +1,11 @@ """Test the Lovelace initialization.""" -from unittest.mock import patch - import pytest from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_capture_events, diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index a44af14d3a0..d32dc9388f1 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -2,11 +2,11 @@ import copy import uuid -from asynctest import patch - from homeassistant.components.lovelace import dashboard, resources from homeassistant.setup import async_setup_component +from tests.async_mock import patch + RESOURCE_EXAMPLES = [ {"type": "js", "url": "/local/bla.js"}, {"type": "css", "url": "/local/bla.css"}, diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 8718db88ce1..70b306d34c0 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,13 +1,13 @@ """Define tests for the Luftdaten config flow.""" from datetime import timedelta -from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.luftdaten import DOMAIN, config_flow from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_duplicate_error(hass): @@ -29,7 +29,7 @@ async def test_communication_error(hass): flow = config_flow.LuftDatenFlowHandler() flow.hass = hass - with patch("luftdaten.Luftdaten.get_data", return_value=mock_coro(None)): + with patch("luftdaten.Luftdaten.get_data", return_value=None): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_SENSOR_ID: "invalid_sensor"} @@ -41,8 +41,8 @@ async def test_invalid_sensor(hass): flow = config_flow.LuftDatenFlowHandler() flow.hass = hass - with patch("luftdaten.Luftdaten.get_data", return_value=mock_coro(False)), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=mock_coro(False) + with patch("luftdaten.Luftdaten.get_data", return_value=False), patch( + "luftdaten.Luftdaten.validate_sensor", return_value=False ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_SENSOR_ID: "invalid_sensor"} @@ -66,8 +66,8 @@ async def test_step_import(hass): flow = config_flow.LuftDatenFlowHandler() flow.hass = hass - with patch("luftdaten.Luftdaten.get_data", return_value=mock_coro(True)), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=mock_coro(True) + with patch("luftdaten.Luftdaten.get_data", return_value=True), patch( + "luftdaten.Luftdaten.validate_sensor", return_value=True ): result = await flow.async_step_import(import_config=conf) @@ -91,8 +91,8 @@ async def test_step_user(hass): flow = config_flow.LuftDatenFlowHandler() flow.hass = hass - with patch("luftdaten.Luftdaten.get_data", return_value=mock_coro(True)), patch( - "luftdaten.Luftdaten.validate_sensor", return_value=mock_coro(True) + with patch("luftdaten.Luftdaten.get_data", return_value=True), patch( + "luftdaten.Luftdaten.validate_sensor", return_value=True ): result = await flow.async_step_user(user_input=conf) diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py index ebe5f73669e..a8ea57cbb6b 100644 --- a/tests/components/luftdaten/test_init.py +++ b/tests/components/luftdaten/test_init.py @@ -1,11 +1,11 @@ """Test the Luftdaten component setup.""" -from unittest.mock import patch - from homeassistant.components import luftdaten from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP from homeassistant.setup import async_setup_component +from tests.async_mock import patch + async def test_config_with_sensor_passed_to_config_entry(hass): """Test that configured options for a sensor are loaded.""" diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py new file mode 100644 index 00000000000..0e0ca8686ef --- /dev/null +++ b/tests/components/lutron_caseta/__init__.py @@ -0,0 +1 @@ +"""Tests for the Lutron Caseta integration.""" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py new file mode 100644 index 00000000000..a528e223e44 --- /dev/null +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the Lutron Caseta config flow.""" +from asynctest import patch +from pylutron_caseta.smartbridge import Smartbridge + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.lutron_caseta import DOMAIN +import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow +from homeassistant.components.lutron_caseta.const import ( + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, + ERROR_CANNOT_CONNECT, + STEP_IMPORT_FAILED, +) +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + + +class MockBridge: + """Mock Lutron bridge that emulates configured connected status.""" + + def __init__(self, can_connect=True): + """Initialize MockBridge instance with configured mock connectivity.""" + self.can_connect = can_connect + self.is_currently_connected = False + + async def connect(self): + """Connect the mock bridge.""" + if self.can_connect: + self.is_currently_connected = True + + def is_connected(self): + """Return whether the mock bridge is connected.""" + return self.is_currently_connected + + async def close(self): + """Close the mock bridge connection.""" + self.is_currently_connected = False + + +async def test_bridge_import_flow(hass): + """Test a bridge entry gets created and set up during the import flow.""" + + entry_mock_data = { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + } + + with patch( + "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, + ) as mock_setup_entry, patch.object(Smartbridge, "create_tls") as create_tls: + create_tls.return_value = MockBridge(can_connect=True) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_mock_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == CasetaConfigFlow.ENTRY_DEFAULT_TITLE + assert result["data"] == entry_mock_data + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bridge_cannot_connect(hass): + """Test checking for connection and cannot_connect error.""" + + entry_mock_data = { + CONF_HOST: "not.a.valid.host", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + } + + with patch( + "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, + ) as mock_setup_entry, patch.object(Smartbridge, "create_tls") as create_tls: + create_tls.return_value = MockBridge(can_connect=False) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_mock_data, + ) + + assert result["type"] == "form" + assert result["step_id"] == STEP_IMPORT_FAILED + assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} + # validate setup_entry was not called + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_duplicate_bridge_import(hass): + """Test that creating a bridge entry with a duplicate host errors.""" + + entry_mock_data = { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + } + mock_entry = MockConfigEntry(domain=DOMAIN, data=entry_mock_data) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, + ) as mock_setup_entry: + # Mock entry added, try initializing flow with duplicate host + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_mock_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == CasetaConfigFlow.ABORT_REASON_ALREADY_CONFIGURED + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index 7ed67dcc0d2..5d6cec844f2 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -1,12 +1,12 @@ """Test the init file of Mailgun.""" import hashlib import hmac -from unittest.mock import Mock import pytest from homeassistant import data_entry_flow from homeassistant.components import mailgun, webhook +from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_API_KEY, CONF_DOMAIN from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -30,7 +30,9 @@ async def webhook_id_with_api_key(hass): {mailgun.DOMAIN: {CONF_API_KEY: API_KEY, CONF_DOMAIN: "example.com"}}, ) - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( "mailgun", context={"source": "user"} ) @@ -47,7 +49,9 @@ async def webhook_id_without_api_key(hass): """Initialize the Mailgun component and get the webhook_id w/o API key.""" await async_setup_component(hass, mailgun.DOMAIN, {}) - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( "mailgun", context={"source": "user"} ) diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index f1596277e3c..d3e26597d84 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1,6 +1,5 @@ """The tests for the manual Alarm Control Panel component.""" from datetime import timedelta -from unittest.mock import MagicMock, patch from homeassistant.components import alarm_control_panel from homeassistant.components.demo import alarm_control_panel as demo @@ -9,6 +8,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -17,6 +17,7 @@ from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed, mock_component, mock_restore_cache from tests.components.alarm_control_panel import common @@ -41,7 +42,7 @@ async def test_arm_home_no_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -67,7 +68,7 @@ async def test_arm_home_no_pending_when_code_not_req(hass): "name": "test", "code": CODE, "code_arm_required": False, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -92,7 +93,7 @@ async def test_arm_home_with_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -104,10 +105,10 @@ async def test_arm_home_with_pending(hass): await common.async_alarm_arm_home(hass, CODE, entity_id) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state state = hass.states.get(entity_id) - assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_HOME + assert state.attributes["next_state"] == STATE_ALARM_ARMED_HOME future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -131,7 +132,7 @@ async def test_arm_home_with_invalid_code(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -156,7 +157,7 @@ async def test_arm_away_no_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -182,7 +183,7 @@ async def test_arm_away_no_pending_when_code_not_req(hass): "name": "test", "code": CODE, "code_arm_required": False, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -207,7 +208,7 @@ async def test_arm_home_with_template_code(hass): "platform": "manual", "name": "test", "code_template": '{{ "abc" }}', - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -233,7 +234,7 @@ async def test_arm_away_with_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -245,10 +246,10 @@ async def test_arm_away_with_pending(hass): await common.async_alarm_arm_away(hass, CODE) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state state = hass.states.get(entity_id) - assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_AWAY + assert state.attributes["next_state"] == STATE_ALARM_ARMED_AWAY future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -272,7 +273,7 @@ async def test_arm_away_with_invalid_code(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -297,7 +298,7 @@ async def test_arm_night_no_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -323,7 +324,7 @@ async def test_arm_night_no_pending_when_code_not_req(hass): "name": "test", "code": CODE, "code_arm_required": False, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -348,7 +349,7 @@ async def test_arm_night_with_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -360,10 +361,10 @@ async def test_arm_night_with_pending(hass): await common.async_alarm_arm_night(hass, CODE, entity_id) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state state = hass.states.get(entity_id) - assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_NIGHT + assert state.attributes["next_state"] == STATE_ALARM_ARMED_NIGHT future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -392,7 +393,7 @@ async def test_arm_night_with_invalid_code(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -452,7 +453,7 @@ async def test_trigger_with_delay(hass): "name": "test", "code": CODE, "delay_time": 1, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -470,7 +471,7 @@ async def test_trigger_with_delay(hass): state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state - assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"] + assert STATE_ALARM_TRIGGERED == state.attributes["next_state"] future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -493,7 +494,7 @@ async def test_trigger_zero_trigger_time(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 0, + "arming_time": 0, "trigger_time": 0, "disarm_after_trigger": False, } @@ -518,7 +519,7 @@ async def test_trigger_zero_trigger_time_with_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 2, + "arming_time": 2, "trigger_time": 0, "disarm_after_trigger": False, } @@ -543,7 +544,7 @@ async def test_trigger_with_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 2, + "delay_time": 2, "trigger_time": 3, "disarm_after_trigger": False, } @@ -559,7 +560,7 @@ async def test_trigger_with_pending(hass): assert STATE_ALARM_PENDING == hass.states.get(entity_id).state state = hass.states.get(entity_id) - assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED + assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED future = dt_util.utcnow() + timedelta(seconds=2) with patch( @@ -595,7 +596,7 @@ async def test_trigger_with_unused_specific_delay(hass): "name": "test", "code": CODE, "delay_time": 5, - "pending_time": 0, + "arming_time": 0, "armed_home": {"delay_time": 10}, "disarm_after_trigger": False, } @@ -614,7 +615,7 @@ async def test_trigger_with_unused_specific_delay(hass): state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state - assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"] + assert STATE_ALARM_TRIGGERED == state.attributes["next_state"] future = dt_util.utcnow() + timedelta(seconds=5) with patch( @@ -639,7 +640,7 @@ async def test_trigger_with_specific_delay(hass): "name": "test", "code": CODE, "delay_time": 10, - "pending_time": 0, + "arming_time": 0, "armed_away": {"delay_time": 1}, "disarm_after_trigger": False, } @@ -658,7 +659,7 @@ async def test_trigger_with_specific_delay(hass): state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state - assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"] + assert STATE_ALARM_TRIGGERED == state.attributes["next_state"] future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -682,9 +683,8 @@ async def test_trigger_with_pending_and_delay(hass): "platform": "manual", "name": "test", "code": CODE, - "delay_time": 1, - "pending_time": 0, - "triggered": {"pending_time": 1}, + "delay_time": 2, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -702,7 +702,7 @@ async def test_trigger_with_pending_and_delay(hass): state = hass.states.get(entity_id) assert state.state == STATE_ALARM_PENDING - assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED + assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -714,7 +714,7 @@ async def test_trigger_with_pending_and_delay(hass): state = hass.states.get(entity_id) assert state.state == STATE_ALARM_PENDING - assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED + assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED future += timedelta(seconds=1) with patch( @@ -739,9 +739,8 @@ async def test_trigger_with_pending_and_specific_delay(hass): "name": "test", "code": CODE, "delay_time": 10, - "pending_time": 0, - "armed_away": {"delay_time": 1}, - "triggered": {"pending_time": 1}, + "arming_time": 0, + "armed_away": {"delay_time": 2}, "disarm_after_trigger": False, } }, @@ -759,7 +758,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): state = hass.states.get(entity_id) assert state.state == STATE_ALARM_PENDING - assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED + assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -771,7 +770,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): state = hass.states.get(entity_id) assert state.state == STATE_ALARM_PENDING - assert state.attributes["post_pending_state"] == STATE_ALARM_TRIGGERED + assert state.attributes["next_state"] == STATE_ALARM_TRIGGERED future += timedelta(seconds=1) with patch( @@ -794,8 +793,8 @@ async def test_armed_home_with_specific_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 10, - "armed_home": {"pending_time": 2}, + "arming_time": 10, + "armed_home": {"arming_time": 2}, } }, ) @@ -804,7 +803,7 @@ async def test_armed_home_with_specific_pending(hass): await common.async_alarm_arm_home(hass) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state future = dt_util.utcnow() + timedelta(seconds=2) with patch( @@ -826,8 +825,8 @@ async def test_armed_away_with_specific_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 10, - "armed_away": {"pending_time": 2}, + "arming_time": 10, + "armed_away": {"arming_time": 2}, } }, ) @@ -836,7 +835,7 @@ async def test_armed_away_with_specific_pending(hass): await common.async_alarm_arm_away(hass) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state future = dt_util.utcnow() + timedelta(seconds=2) with patch( @@ -858,8 +857,8 @@ async def test_armed_night_with_specific_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 10, - "armed_night": {"pending_time": 2}, + "arming_time": 10, + "armed_night": {"arming_time": 2}, } }, ) @@ -868,7 +867,7 @@ async def test_armed_night_with_specific_pending(hass): await common.async_alarm_arm_night(hass) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state future = dt_util.utcnow() + timedelta(seconds=2) with patch( @@ -890,8 +889,8 @@ async def test_trigger_with_specific_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 10, - "triggered": {"pending_time": 2}, + "delay_time": 10, + "disarmed": {"delay_time": 2}, "trigger_time": 3, "disarm_after_trigger": False, } @@ -935,7 +934,7 @@ async def test_trigger_with_disarm_after_trigger(hass): "platform": "manual", "name": "test", "trigger_time": 5, - "pending_time": 0, + "delay_time": 0, "disarm_after_trigger": True, } }, @@ -971,7 +970,7 @@ async def test_trigger_with_zero_specific_trigger_time(hass): "name": "test", "trigger_time": 5, "disarmed": {"trigger_time": 0}, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": True, } }, @@ -997,7 +996,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass): "name": "test", "trigger_time": 5, "armed_home": {"trigger_time": 0}, - "pending_time": 0, + "delay_time": 0, "disarm_after_trigger": True, } }, @@ -1032,7 +1031,7 @@ async def test_trigger_with_specific_trigger_time(hass): "platform": "manual", "name": "test", "disarmed": {"trigger_time": 5}, - "pending_time": 0, + "delay_time": 0, "disarm_after_trigger": True, } }, @@ -1067,7 +1066,8 @@ async def test_trigger_with_no_disarm_after_trigger(hass): "platform": "manual", "name": "test", "trigger_time": 5, - "pending_time": 0, + "arming_time": 0, + "delay_time": 0, "disarm_after_trigger": False, } }, @@ -1106,7 +1106,8 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): "platform": "manual", "name": "test", "trigger_time": 5, - "pending_time": 0, + "arming_time": 0, + "delay_time": 0, "disarm_after_trigger": False, } }, @@ -1196,7 +1197,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 5, + "delay_time": 5, "code": CODE + "2", "disarm_after_trigger": False, } @@ -1236,7 +1237,7 @@ async def test_disarm_with_template_code(hass): "platform": "manual", "name": "test", "code_template": '{{ "" if from_state == "disarmed" else "abc" }}', - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -1272,7 +1273,7 @@ async def test_arm_custom_bypass_no_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -1298,7 +1299,7 @@ async def test_arm_custom_bypass_no_pending_when_code_not_req(hass): "name": "test", "code": CODE, "code_arm_required": False, - "pending_time": 0, + "arming_time": 0, "disarm_after_trigger": False, } }, @@ -1323,7 +1324,7 @@ async def test_arm_custom_bypass_with_pending(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -1335,10 +1336,10 @@ async def test_arm_custom_bypass_with_pending(hass): await common.async_alarm_arm_custom_bypass(hass, CODE, entity_id) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state state = hass.states.get(entity_id) - assert state.attributes["post_pending_state"] == STATE_ALARM_ARMED_CUSTOM_BYPASS + assert state.attributes["next_state"] == STATE_ALARM_ARMED_CUSTOM_BYPASS future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -1362,7 +1363,7 @@ async def test_arm_custom_bypass_with_invalid_code(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 1, + "arming_time": 1, "disarm_after_trigger": False, } }, @@ -1386,8 +1387,8 @@ async def test_armed_custom_bypass_with_specific_pending(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 10, - "armed_custom_bypass": {"pending_time": 2}, + "arming_time": 10, + "armed_custom_bypass": {"arming_time": 2}, } }, ) @@ -1396,7 +1397,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass): await common.async_alarm_arm_custom_bypass(hass) - assert STATE_ALARM_PENDING == hass.states.get(entity_id).state + assert STATE_ALARM_ARMING == hass.states.get(entity_id).state future = dt_util.utcnow() + timedelta(seconds=2) with patch( @@ -1419,9 +1420,9 @@ async def test_arm_away_after_disabled_disarmed(hass): "platform": "manual", "name": "test", "code": CODE, - "pending_time": 0, + "arming_time": 0, "delay_time": 1, - "armed_away": {"pending_time": 1}, + "armed_away": {"arming_time": 1}, "disarmed": {"trigger_time": 0}, "disarm_after_trigger": False, } @@ -1435,16 +1436,16 @@ async def test_arm_away_after_disabled_disarmed(hass): await common.async_alarm_arm_away(hass, CODE) state = hass.states.get(entity_id) - assert STATE_ALARM_PENDING == state.state - assert STATE_ALARM_DISARMED == state.attributes["pre_pending_state"] - assert STATE_ALARM_ARMED_AWAY == state.attributes["post_pending_state"] + assert STATE_ALARM_ARMING == state.state + assert STATE_ALARM_DISARMED == state.attributes["previous_state"] + assert STATE_ALARM_ARMED_AWAY == state.attributes["next_state"] await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) - assert STATE_ALARM_PENDING == state.state - assert STATE_ALARM_DISARMED == state.attributes["pre_pending_state"] - assert STATE_ALARM_ARMED_AWAY == state.attributes["post_pending_state"] + assert STATE_ALARM_ARMING == state.state + assert STATE_ALARM_DISARMED == state.attributes["previous_state"] + assert STATE_ALARM_ARMED_AWAY == state.attributes["next_state"] future = dt_util.utcnow() + timedelta(seconds=1) with patch( @@ -1461,8 +1462,8 @@ async def test_arm_away_after_disabled_disarmed(hass): state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state - assert STATE_ALARM_ARMED_AWAY == state.attributes["pre_pending_state"] - assert STATE_ALARM_TRIGGERED == state.attributes["post_pending_state"] + assert STATE_ALARM_ARMED_AWAY == state.attributes["previous_state"] + assert STATE_ALARM_TRIGGERED == state.attributes["next_state"] future += timedelta(seconds=1) with patch( @@ -1492,7 +1493,7 @@ async def test_restore_armed_state(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 0, + "arming_time": 0, "trigger_time": 0, "disarm_after_trigger": False, } @@ -1518,7 +1519,7 @@ async def test_restore_disarmed_state(hass): "alarm_control_panel": { "platform": "manual", "name": "test", - "pending_time": 0, + "arming_time": 0, "trigger_time": 0, "disarm_after_trigger": False, } diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 1ac6bca91c4..bff3818af56 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1,7 +1,6 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta import unittest -from unittest.mock import Mock, patch from homeassistant.components import alarm_control_panel from homeassistant.const import ( @@ -15,6 +14,7 @@ from homeassistant.const import ( from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( assert_setup_component, fire_mqtt_message, diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 70a29fe11e1..637ed1900b8 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -1,4 +1,5 @@ """The tests for the MaryTTS speech platform.""" +import asyncio import os import shutil from urllib.parse import urlencode @@ -11,6 +12,7 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) import homeassistant.components.tts as tts +from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.setup import setup_component @@ -24,6 +26,13 @@ class TestTTSMaryTTSPlatform: """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + asyncio.run_coroutine_threadsafe( + async_process_ha_core_config( + self.hass, {"internal_url": "http://example.local:8123"} + ), + self.hass.loop, + ) + self.host = "localhost" self.port = 59125 self.params = { diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 2cbca449ed6..ac0d70bded9 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -14,7 +14,7 @@ from homeassistant.const import ( from tests.common import get_test_home_assistant -class AsyncMediaPlayer(mp.MediaPlayerDevice): +class AsyncMediaPlayer(mp.MediaPlayerEntity): """Async media player test class.""" def __init__(self, hass): @@ -65,7 +65,7 @@ class AsyncMediaPlayer(mp.MediaPlayerDevice): self._state = STATE_OFF -class SyncMediaPlayer(mp.MediaPlayerDevice): +class SyncMediaPlayer(mp.MediaPlayerEntity): """Sync media player test class.""" def __init__(self, hass): diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index f3d8ec3298a..50924af5d76 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,12 +1,11 @@ """Test the base functions of the media player.""" import base64 -from asynctest import patch - +from homeassistant.components import media_player from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch async def test_get_image(hass, hass_ws_client, caplog): @@ -18,9 +17,9 @@ async def test_get_image(hass, hass_ws_client, caplog): client = await hass_ws_client(hass) with patch( - "homeassistant.components.media_player.MediaPlayerDevice." + "homeassistant.components.media_player.MediaPlayerEntity." "async_get_media_image", - return_value=mock_coro((b"image", "image/jpeg")), + return_value=(b"image", "image/jpeg"), ): await client.send_json( { @@ -53,7 +52,7 @@ async def test_get_image_http(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) with patch( - "homeassistant.components.media_player.MediaPlayerDevice." + "homeassistant.components.media_player.MediaPlayerEntity." "async_get_media_image", return_value=(b"image", "image/jpeg"), ): @@ -66,7 +65,7 @@ async def test_get_image_http(hass, aiohttp_client): async def test_get_image_http_remote(hass, aiohttp_client): """Test get image url via http command.""" with patch( - "homeassistant.components.media_player.MediaPlayerDevice." + "homeassistant.components.media_player.MediaPlayerEntity." "media_image_remotely_accessible", return_value=True, ): @@ -80,7 +79,7 @@ async def test_get_image_http_remote(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) with patch( - "homeassistant.components.media_player.MediaPlayerDevice." + "homeassistant.components.media_player.MediaPlayerEntity." "async_get_media_image", return_value=(b"image", "image/jpeg"), ): @@ -88,3 +87,13 @@ async def test_get_image_http_remote(hass, aiohttp_client): content = await resp.read() assert content == b"image" + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomMediaPlayer(media_player.MediaPlayerDevice): + pass + + CustomMediaPlayer() + assert "MediaPlayerDevice is deprecated, modify CustomMediaPlayer" in caplog.text diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index c936807484a..e6b36306986 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -2,7 +2,6 @@ import asyncio from aiohttp import ClientError, ClientResponseError -from asynctest import patch import pymelcloud import pytest @@ -10,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.melcloud.const import DOMAIN from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 8976f85f3d1..874cc29ab7a 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -1,6 +1,5 @@ """Test for Melissa climate component.""" import json -from unittest.mock import Mock, patch from homeassistant.components.climate.const import ( HVAC_MODE_COOL, @@ -16,7 +15,8 @@ from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from tests.common import load_fixture, mock_coro_func +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import load_fixture _SERIAL = "12345678" @@ -24,17 +24,17 @@ _SERIAL = "12345678" def melissa_mock(): """Use this to mock the melissa api.""" api = Mock() - api.async_fetch_devices = mock_coro_func( + api.async_fetch_devices = AsyncMock( return_value=json.loads(load_fixture("melissa_fetch_devices.json")) ) - api.async_status = mock_coro_func( + api.async_status = AsyncMock( return_value=json.loads(load_fixture("melissa_status.json")) ) - api.async_cur_settings = mock_coro_func( + api.async_cur_settings = AsyncMock( return_value=json.loads(load_fixture("melissa_cur_settings.json")) ) - api.async_send = mock_coro_func(return_value=True) + api.async_send = AsyncMock(return_value=True) api.STATE_OFF = 0 api.STATE_ON = 1 @@ -276,7 +276,7 @@ async def test_send(hass): await thermostat.async_send({"fan": api.FAN_MEDIUM}) await hass.async_block_till_done() assert SPEED_MEDIUM == thermostat.fan_mode - api.async_send.return_value = mock_coro_func(return_value=False) + api.async_send.return_value = AsyncMock(return_value=False) thermostat._cur_settings = None await thermostat.async_send({"fan": api.FAN_LOW}) await hass.async_block_till_done() @@ -296,7 +296,7 @@ async def test_update(hass): await thermostat.async_update() assert SPEED_LOW == thermostat.fan_mode assert HVAC_MODE_HEAT == thermostat.state - api.async_status = mock_coro_func(exception=KeyError("boom")) + api.async_status = AsyncMock(side_effect=KeyError("boom")) await thermostat.async_update() mocked_warning.assert_called_once_with( "Unable to update entity %s", thermostat.entity_id diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index 892f4d60a44..7e174a4f8a0 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,23 +1,22 @@ """The test for the Melissa Climate component.""" from homeassistant.components import melissa -from tests.common import MockDependency, mock_coro_func +from tests.async_mock import AsyncMock, patch VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} async def test_setup(hass): """Test setting up the Melissa component.""" - with MockDependency("melissa") as mocked_melissa: - melissa.melissa = mocked_melissa - mocked_melissa.AsyncMelissa().async_connect = mock_coro_func() + with patch("melissa.AsyncMelissa") as mocked_melissa, patch.object( + melissa, "async_load_platform" + ): + mocked_melissa.return_value.async_connect = AsyncMock() await melissa.async_setup(hass, VALID_CONFIG) - mocked_melissa.AsyncMelissa.assert_called_with( - username="********", password="********" - ) + mocked_melissa.assert_called_with(username="********", password="********") assert melissa.DATA_MELISSA in hass.data assert isinstance( - hass.data[melissa.DATA_MELISSA], type(mocked_melissa.AsyncMelissa()) + hass.data[melissa.DATA_MELISSA], type(mocked_melissa.return_value), ) diff --git a/tests/components/met/conftest.py b/tests/components/met/conftest.py index e475889863d..164a8498465 100644 --- a/tests/components/met/conftest.py +++ b/tests/components/met/conftest.py @@ -1,9 +1,7 @@ """Fixtures for Met weather testing.""" -from unittest.mock import patch - import pytest -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch @pytest.fixture @@ -11,7 +9,7 @@ def mock_weather(): """Mock weather data.""" with patch("metno.MetWeatherData") as mock_data: mock_data = mock_data.return_value - mock_data.fetching_data.side_effect = lambda: mock_coro(True) + mock_data.fetching_data = AsyncMock(return_value=True) mock_data.get_current_weather.return_value = { "condition": "cloudy", "temperature": 15, diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 980994f3fb2..a4c48f38245 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for Met.no config flow.""" -from asynctest import patch import pytest from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py index 088587ab2c2..75c294775ed 100644 --- a/tests/components/meteo_france/conftest.py +++ b/tests/components/meteo_france/conftest.py @@ -1,8 +1,8 @@ """Meteo-France generic test utils.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def patch_requests(): diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index f9ead2c1ef3..d4381073209 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the Meteo-France config flow.""" -from unittest.mock import patch - from meteofrance.client import meteofranceError import pytest @@ -8,6 +6,7 @@ from homeassistant import data_entry_flow from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from tests.async_mock import patch from tests.common import MockConfigEntry CITY_1_POSTAL = "74220" diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 05f175fc191..ff9c7fa7182 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the mFi sensor platform.""" import unittest -import unittest.mock as mock from mficlient.client import FailedToLogin import requests @@ -10,6 +9,7 @@ import homeassistant.components.sensor as sensor from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component +import tests.async_mock as mock from tests.common import get_test_home_assistant diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 42469b1b5ac..414e0b8b50b 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -1,11 +1,11 @@ """The tests for the mFi switch platform.""" import unittest -import unittest.mock as mock import homeassistant.components.mfi.switch as mfi import homeassistant.components.switch as switch from homeassistant.setup import setup_component +import tests.async_mock as mock from tests.common import get_test_home_assistant diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index 598144f5a25..05d462f02a0 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -1,6 +1,5 @@ """Tests for MH-Z19 sensor.""" import unittest -from unittest.mock import DEFAULT, Mock, patch import homeassistant.components.mhz19.sensor as mhz19 from homeassistant.components.sensor import DOMAIN @@ -11,6 +10,7 @@ from homeassistant.const import ( ) from homeassistant.setup import setup_component +from tests.async_mock import DEFAULT, Mock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index 803ca006965..abd35eca3e1 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -1,6 +1,5 @@ """The tests for the microsoft face platform.""" import asyncio -from unittest.mock import patch from homeassistant.components import camera, microsoft_face as mf from homeassistant.components.microsoft_face import ( @@ -18,12 +17,8 @@ from homeassistant.components.microsoft_face import ( from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - load_fixture, - mock_coro, -) +from tests.async_mock import patch +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture def create_group(hass, name): @@ -97,7 +92,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_setup_component(self, mock_update): """Set up component.""" @@ -106,7 +101,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_setup_component_wrong_api_key(self, mock_update): """Set up component without api key.""" @@ -115,7 +110,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_setup_component_test_service(self, mock_update): """Set up component.""" @@ -171,7 +166,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_service_groups(self, mock_update, aioclient_mock): """Set up component, test groups services.""" @@ -258,7 +253,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_service_train(self, mock_update, aioclient_mock): """Set up component, test train groups services.""" @@ -278,7 +273,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"Test")), + return_value=camera.Image("image/jpeg", b"Test"), ) def test_service_face(self, camera_mock, aioclient_mock): """Set up component, test person face services.""" @@ -318,7 +313,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_service_status_400(self, mock_update, aioclient_mock): """Set up component, test groups services with error.""" @@ -340,7 +335,7 @@ class TestMicrosoftFaceSetup: @patch( "homeassistant.components.microsoft_face.MicrosoftFace.update_store", - return_value=mock_coro(), + return_value=None, ) def test_service_status_timeout(self, mock_update, aioclient_mock): """Set up component, test groups services with timeout.""" diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 37dbfad4d35..b47bb941af4 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -1,6 +1,5 @@ """Test Mikrotik setup process.""" from datetime import timedelta -from unittest.mock import patch import librouteros import pytest @@ -16,6 +15,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) +from tests.async_mock import patch from tests.common import MockConfigEntry DEMO_USER_INPUT = { diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 9a2f75f7015..ecfb9add717 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -1,5 +1,4 @@ """Test Mikrotik hub.""" -from asynctest import patch import librouteros from homeassistant import config_entries @@ -7,6 +6,7 @@ from homeassistant.components import mikrotik from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index ea7e22239b2..b3dca7269eb 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -1,12 +1,11 @@ """Test Mikrotik setup process.""" -from unittest.mock import Mock, patch - from homeassistant.components import mikrotik from homeassistant.setup import async_setup_component from . import MOCK_DATA -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import MockConfigEntry async def test_setup_with_no_config(hass): @@ -23,9 +22,9 @@ async def test_successful_config_entry(hass): with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_coro(mock_registry), + return_value=mock_registry, ): - mock_hub.return_value.async_setup.return_value = mock_coro(True) + mock_hub.return_value.async_setup = AsyncMock(return_value=True) mock_hub.return_value.serial_num = "12345678" mock_hub.return_value.model = "RB750" mock_hub.return_value.hostname = "mikrotik" @@ -55,7 +54,7 @@ async def test_hub_fail_setup(hass): entry.add_to_hass(hass) with patch.object(mikrotik, "MikrotikHub") as mock_hub: - mock_hub.return_value.async_setup.return_value = mock_coro(False) + mock_hub.return_value.async_setup = AsyncMock(return_value=False) assert await mikrotik.async_setup_entry(hass, entry) is False assert mikrotik.DOMAIN not in hass.data @@ -67,10 +66,9 @@ async def test_unload_entry(hass): entry.add_to_hass(hass) with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( - "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_coro(Mock()), + "homeassistant.helpers.device_registry.async_get_registry", return_value=Mock(), ): - mock_hub.return_value.async_setup.return_value = mock_coro(True) + mock_hub.return_value.async_setup = AsyncMock(return_value=True) mock_hub.return_value.serial_num = "12345678" mock_hub.return_value.model = "RB750" mock_hub.return_value.hostname = "mikrotik" diff --git a/tests/components/mill/__init__.py b/tests/components/mill/__init__.py new file mode 100644 index 00000000000..4bbe1b4fb08 --- /dev/null +++ b/tests/components/mill/__init__.py @@ -0,0 +1 @@ +"""Tests for Mill.""" diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py new file mode 100644 index 00000000000..54b8dbc9b59 --- /dev/null +++ b/tests/components/mill/test_config_flow.py @@ -0,0 +1,86 @@ +"""Tests for Mill config flow.""" +import pytest + +from homeassistant.components.mill.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mill_setup", autouse=True) +def mill_setup_fixture(): + """Patch mill setup entry.""" + with patch("homeassistant.components.mill.async_setup_entry", return_value=True): + yield + + +async def test_show_config_form(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_create_entry(hass): + """Test create entry from user input.""" + test_data = { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + } + + with patch("mill.Mill.connect", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "create_entry" + assert result["title"] == test_data[CONF_USERNAME] + assert result["data"] == test_data + + +async def test_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists.""" + + test_data = { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + } + + first_entry = MockConfigEntry( + domain="mill", data=test_data, unique_id=test_data[CONF_USERNAME], + ) + first_entry.add_to_hass(hass) + + with patch("mill.Mill.connect", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test connection error.""" + + test_data = { + CONF_USERNAME: "user", + CONF_PASSWORD: "pswd", + } + + first_entry = MockConfigEntry( + domain="mill", data=test_data, unique_id=test_data[CONF_USERNAME], + ) + first_entry.add_to_hass(hass) + + with patch("mill.Mill.connect", return_value=False): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "form" + assert result["errors"]["connection_error"] == "connection_error" diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index bc49ed08109..17ec9080e86 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -3,7 +3,6 @@ import asyncio import aiodns -from asynctest import patch from mcstatus.pinger import PingResponse from homeassistant.components.minecraft_server.const import ( @@ -20,6 +19,7 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.common import MockConfigEntry @@ -68,12 +68,12 @@ USER_INPUT_IPV6 = { USER_INPUT_PORT_TOO_SMALL = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: f"mc.dummyserver.com:1023", + CONF_HOST: "mc.dummyserver.com:1023", } USER_INPUT_PORT_TOO_LARGE = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: f"mc.dummyserver.com:65536", + CONF_HOST: "mc.dummyserver.com:65536", } SRV_RECORDS = asyncio.Future() diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 4397f446a19..88f7418d2d4 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -1,9 +1,7 @@ """Tests for Minio Hass related code.""" import asyncio import json -from unittest.mock import MagicMock -from asynctest import call, patch import pytest from homeassistant.components.minio import ( @@ -20,6 +18,7 @@ from homeassistant.components.minio import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, call, patch from tests.components.minio.common import TEST_EVENT diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 631c5b40734..2dd385f0253 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -1,6 +1,5 @@ """The tests for the mochad light platform.""" import unittest -import unittest.mock as mock import pytest @@ -8,6 +7,7 @@ from homeassistant.components import light from homeassistant.components.mochad import light as mochad from homeassistant.setup import setup_component +import tests.async_mock as mock from tests.common import get_test_home_assistant diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index aa6ce354a32..699edfe899c 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -1,6 +1,5 @@ """The tests for the mochad switch platform.""" import unittest -import unittest.mock as mock import pytest @@ -8,6 +7,7 @@ from homeassistant.components import switch from homeassistant.components.mochad import switch as mochad from homeassistant.setup import setup_component +import tests.async_mock as mock from tests.common import get_test_home_assistant diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 6207a363937..ab4d745dc50 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.modbus.const import ( CONF_SCALE, DATA_TYPE_FLOAT, DATA_TYPE_INT, + DATA_TYPE_STRING, DATA_TYPE_UINT, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -357,3 +358,23 @@ async def test_float_data_type(hass, mock_hub): register_words=[16286, 1617], expected="1.23457", ) + + +async def test_string_data_type(hass, mock_hub): + """Test byte string register data type.""" + register_config = { + CONF_COUNT: 8, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + SENSOR_DOMAIN, + register_words=[0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335], + expected="07-05-2020 14:35", + ) diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index ecafa17e174..8c6e2a3916c 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Monoprice 6-Zone Amplifier config flow.""" -from asynctest import patch from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup @@ -12,6 +11,7 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT +from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 0006364b94e..ccd70c628e2 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,7 +1,6 @@ """The tests for Monoprice Media player platform.""" from collections import defaultdict -from asynctest import patch from serial import SerialException from homeassistant.components.media_player.const import ( @@ -18,6 +17,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.monoprice.const import ( + CONF_NOT_FIRST_RUN, CONF_SOURCES, DOMAIN, SERVICE_RESTORE, @@ -34,6 +34,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_component import async_update_entity +from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} @@ -41,6 +42,7 @@ MOCK_OPTIONS = {CONF_SOURCES: {"2": "two", "4": "four"}} ZONE_1_ID = "media_player.zone_11" ZONE_2_ID = "media_player.zone_12" +ZONE_7_ID = "media_player.zone_21" class AttrDict(dict): @@ -100,8 +102,6 @@ async def test_cannot_connect(hass): config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - # setup_component(self.hass, DOMAIN, MOCK_CONFIG) - # self.hass.async_block_till_done() await hass.async_block_till_done() assert hass.states.get(ZONE_1_ID) is None @@ -113,8 +113,6 @@ async def _setup_monoprice(hass, monoprice): config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - # setup_component(self.hass, DOMAIN, MOCK_CONFIG) - # self.hass.async_block_till_done() await hass.async_block_till_done() @@ -127,8 +125,17 @@ async def _setup_monoprice_with_options(hass, monoprice): ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - # setup_component(self.hass, DOMAIN, MOCK_CONFIG) - # self.hass.async_block_till_done() + await hass.async_block_till_done() + + +async def _setup_monoprice_not_first_run(hass, monoprice): + with patch( + "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, + ): + data = {**MOCK_CONFIG, CONF_NOT_FIRST_RUN: True} + config_entry = MockConfigEntry(domain=DOMAIN, data=data) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -479,3 +486,47 @@ async def test_volume_up_down(hass): hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} ) assert monoprice.zones[11].volume == 37 + + +async def test_first_run_with_available_zones(hass): + """Test first run with all zones available.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get(ZONE_7_ID) + assert not entry.disabled + + +async def test_first_run_with_failing_zones(hass): + """Test first run with failed zones.""" + monoprice = MockMonoprice() + + with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): + await _setup_monoprice(hass, monoprice) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get(ZONE_1_ID) + assert not entry.disabled + + entry = registry.async_get(ZONE_7_ID) + assert entry.disabled + assert entry.disabled_by == "integration" + + +async def test_not_first_run_with_failing_zone(hass): + """Test first run with failed zones.""" + monoprice = MockMonoprice() + + with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): + await _setup_monoprice_not_first_run(hass, monoprice) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get(ZONE_1_ID) + assert not entry.disabled + + entry = registry.async_get(ZONE_7_ID) + assert not entry.disabled diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 1e19d0a4d83..89d5a8f798c 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,11 +1,11 @@ """The test for the moon sensor platform.""" from datetime import datetime import unittest -from unittest.mock import patch from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import get_test_home_assistant DAY1 = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 254449cc129..6677122cf10 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -2,6 +2,8 @@ import copy import json +import pytest + from homeassistant.components import alarm_control_panel from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -551,6 +553,7 @@ async def test_discovery_update_alarm(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 07a1c8b6e5f..31acf187ad5 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -2,9 +2,11 @@ import copy from datetime import datetime, timedelta import json -from unittest.mock import patch -from homeassistant.components import binary_sensor +import pytest + +from homeassistant.components import binary_sensor, mqtt +from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, @@ -37,7 +39,12 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message, async_fire_time_changed +from tests.async_mock import patch +from tests.common import ( + MockConfigEntry, + async_fire_mqtt_message, + async_fire_time_changed, +) DEFAULT_CONFIG = { binary_sensor.DOMAIN: { @@ -70,8 +77,9 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, async_fire_mqtt_message(hass, "availability-topic", "online") + # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("binary_sensor.test") - assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_UNAVAILABLE await expires_helper(hass, mqtt_mock, caplog) @@ -92,15 +100,15 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) + # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("binary_sensor.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNAVAILABLE await expires_helper(hass, mqtt_mock, caplog) async def expires_helper(hass, mqtt_mock, caplog): """Run the basic expiry code.""" - now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) @@ -178,6 +186,43 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.state == STATE_OFF +async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): + """Test the setting of the value via MQTT.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } + }, + ) + + state = hass.states.get("binary_sensor.test") + + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test-topic", "0N") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + assert "No matching payload found for entity" in caplog.text + caplog.clear() + assert "No matching payload found for entity" not in caplog.text + + async_fire_mqtt_message(hass, "test-topic", "ON") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test-topic", "0FF") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + assert "No matching payload found for entity" in caplog.text + + async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_mock): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -460,6 +505,89 @@ async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): ) +async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( + hass, mqtt_mock, caplog +): + """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, "homeassistant", {}, entry) + + config = { + "name": "Test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + } + + config_msg = json.dumps(config) + + # Set time and publish config message to create binary_sensor via discovery with 4 s expiry + now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message( + hass, "homeassistant/binary_sensor/bla/config", config_msg + ) + await hass.async_block_till_done() + + # Test that binary_sensor is not available + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + # Publish state message + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): + async_fire_mqtt_message(hass, "test-topic", "ON") + await hass.async_block_till_done() + + # Test that binary_sensor has correct state + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Advance +3 seconds + now = now + timedelta(seconds=3) + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # binary_sensor is not yet expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Resend config message to update discovery + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message( + hass, "homeassistant/binary_sensor/bla/config", config_msg + ) + await hass.async_block_till_done() + + # Test that binary_sensor has not expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Add +2 seconds + now = now + timedelta(seconds=2) + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Test that binary_sensor has expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + # Resend config message to update discovery + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): + async_fire_mqtt_message( + hass, "homeassistant/binary_sensor/bla/config", config_msg + ) + await hass.async_block_till_done() + + # Test that binary_sensor is still expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "off_delay": -1 }' diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index cefeda04097..5747d876b57 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,6 +1,8 @@ """The tests for mqtt camera component.""" import json +import pytest + from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component @@ -155,6 +157,7 @@ async def test_discovery_update_camera(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 1d485c4be13..8c3bfebed20 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1,7 +1,6 @@ """The tests for the mqtt climate component.""" import copy import json -import unittest import pytest import voluptuous as vol @@ -47,6 +46,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import call from tests.common import async_fire_mqtt_message, async_setup_component from tests.components.climate import common @@ -180,10 +180,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.state == "cool" mqtt_mock.async_publish.assert_has_calls( - [ - unittest.mock.call("power-command", "ON", 0, False), - unittest.mock.call("mode-topic", "cool", 0, False), - ] + [call("power-command", "ON", 0, False), call("mode-topic", "cool", 0, False)] ) mqtt_mock.async_publish.reset_mock() @@ -191,10 +188,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" mqtt_mock.async_publish.assert_has_calls( - [ - unittest.mock.call("power-command", "OFF", 0, False), - unittest.mock.call("mode-topic", "off", 0, False), - ] + [call("power-command", "OFF", 0, False), call("mode-topic", "off", 0, False)] ) mqtt_mock.async_publish.reset_mock() @@ -322,10 +316,7 @@ async def test_set_target_temperature(hass, mqtt_mock): assert state.state == "cool" assert state.attributes.get("temperature") == 21 mqtt_mock.async_publish.assert_has_calls( - [ - unittest.mock.call("mode-topic", "cool", 0, False), - unittest.mock.call("temperature-topic", 21, 0, False), - ] + [call("mode-topic", "cool", 0, False), call("temperature-topic", 21, 0, False)] ) mqtt_mock.async_publish.reset_mock() @@ -465,10 +456,7 @@ async def test_set_away_mode(hass, mqtt_mock): await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) mqtt_mock.async_publish.assert_has_calls( - [ - unittest.mock.call("hold-topic", "off", 0, False), - unittest.mock.call("away-mode-topic", "AN", 0, False), - ] + [call("hold-topic", "off", 0, False), call("away-mode-topic", "AN", 0, False)] ) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "away" @@ -880,6 +868,7 @@ async def test_discovery_update_climate(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "power_command_topic": "test_topic#" }' diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 1199aaa40c7..bfd478a712e 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -3,13 +3,13 @@ import copy from datetime import datetime import json from unittest import mock -from unittest.mock import ANY from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE +from tests.async_mock import ANY from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -252,7 +252,6 @@ async def help_test_unique_id(hass, domain, config): """Test unique id option only creates one entity per unique_id.""" await async_mock_mqtt_component(hass) assert await async_setup_component(hass, domain, config,) - async_fire_mqtt_message(hass, "test-topic", "payload") assert len(hass.states.async_entity_ids(domain)) == 1 @@ -601,7 +600,13 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf == debug_info.STORED_MESSAGES ) messages = [ - {"topic": "test-topic", "payload": f"{i}", "time": start_dt} + { + "payload": f"{i}", + "qos": 0, + "retain": False, + "time": start_dt, + "topic": "test-topic", + } for i in range(1, debug_info.STORED_MESSAGES + 1) ] assert {"topic": "test-topic", "messages": messages} in debug_info_data["entities"][ @@ -656,7 +661,15 @@ async def help_test_entity_debug_info_message( assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert { "topic": topic, - "messages": [{"topic": topic, "payload": payload, "time": start_dt}], + "messages": [ + { + "payload": payload, + "qos": 0, + "retain": False, + "time": start_dt, + "topic": topic, + } + ], } in debug_info_data["entities"][0]["subscriptions"] diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index aa72549152e..0990accec9f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,18 +1,18 @@ """Test config flow.""" -from unittest.mock import patch import pytest from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) def mock_finish_setup(): """Mock out the finish setup method.""" with patch( - "homeassistant.components.mqtt.MQTT.async_connect", return_value=mock_coro(True) + "homeassistant.components.mqtt.MQTT.async_connect", return_value=True ) as mock_finish: yield mock_finish diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 6de462b9020..201bb17c7a8 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,4 +1,6 @@ """The tests for the MQTT cover platform.""" +import pytest + from homeassistant.components import cover from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -1831,6 +1833,7 @@ async def test_discovery_update_cover(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic#" }' diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index ababe8395f3..32f826422a8 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,11 +1,11 @@ """The tests for the MQTT device tracker platform.""" -from asynctest import patch import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 7274badbed9..f77ccca57ef 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -134,6 +134,7 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock assert_lists_same(triggers, []) +@pytest.mark.no_fail_on_log_exception async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): """Test bad discovery message.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 1b2f76d6c5e..8c75d77efb8 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,7 +1,6 @@ """The tests for the MQTT discovery.""" from pathlib import Path import re -from unittest.mock import patch import pytest @@ -13,10 +12,10 @@ from homeassistant.components.mqtt.abbreviations import ( from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import STATE_OFF, STATE_ON +from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, - mock_coro, mock_device_registry, mock_registry, ) @@ -57,7 +56,7 @@ async def test_invalid_topic(hass, mqtt_mock): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - mock_dispatcher_send.return_value = mock_coro() + mock_dispatcher_send = AsyncMock(return_value=None) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( @@ -76,7 +75,7 @@ async def test_invalid_json(hass, mqtt_mock, caplog): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - mock_dispatcher_send.return_value = mock_coro() + mock_dispatcher_send = AsyncMock(return_value=None) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( @@ -96,7 +95,7 @@ async def test_only_valid_components(hass, mqtt_mock, caplog): invalid_component = "timer" - mock_dispatcher_send.return_value = mock_coro() + mock_dispatcher_send = AsyncMock(return_value=None) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 6ef1c0aab86..d8b6ce00ee6 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,4 +1,6 @@ """Test MQTT fans.""" +import pytest + from homeassistant.components import fan from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -681,6 +683,7 @@ async def test_discovery_update_fan(hass, mqtt_mock, caplog): await help_test_discovery_update(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index fa7d49482c8..9ec5e09f276 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import json import ssl import unittest -from unittest import mock import pytest import voluptuous as vol @@ -24,6 +23,7 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, MagicMock, call, mock_open, patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -31,16 +31,19 @@ from tests.common import ( async_mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, - mock_coro, mock_device_registry, mock_mqtt_component, mock_registry, - mock_storage, threadsafe_coroutine_factory, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES +@pytest.fixture(autouse=True) +def mock_storage(hass_storage): + """Autouse hass_storage for the TestCase tests.""" + + @pytest.fixture def device_reg(hass): """Return an empty, loaded, registry.""" @@ -56,9 +59,9 @@ def entity_reg(hass): @pytest.fixture def mock_mqtt(): """Make sure connection is established.""" - with mock.patch("homeassistant.components.mqtt.MQTT") as mock_mqtt: - mock_mqtt.return_value.async_connect.return_value = mock_coro(True) - mock_mqtt.return_value.async_disconnect.return_value = mock_coro(True) + with patch("homeassistant.components.mqtt.MQTT") as mock_mqtt: + mock_mqtt.return_value.async_connect = AsyncMock(return_value=True) + mock_mqtt.return_value.async_disconnect = AsyncMock(return_value=True) yield mock_mqtt @@ -67,7 +70,7 @@ async def async_mock_mqtt_client(hass, config=None): if config is None: config = {mqtt.CONF_BROKER: "mock-broker"} - with mock.patch("paho.mqtt.client.Client") as mock_client: + with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect.return_value = 0 mock_client().subscribe.return_value = (0, 0) mock_client().unsubscribe.return_value = (0, 0) @@ -88,15 +91,12 @@ class TestMQTTComponent(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() mock_mqtt_component(self.hass) self.calls = [] def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - self.mock_storage.__exit__(None, None, None) @callback def record_calls(self, *args): @@ -306,15 +306,12 @@ class TestMQTTCallbacks(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() mock_mqtt_client(self.hass) self.calls = [] def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - self.mock_storage.__exit__(None, None, None) @callback def record_calls(self, *args): @@ -583,12 +580,12 @@ class TestMQTTCallbacks(unittest.TestCase): # Fake that the client is connected self.hass.data["mqtt"].connected = True - calls_a = mock.MagicMock() + calls_a = MagicMock() mqtt.subscribe(self.hass, "test/state", calls_a) self.hass.block_till_done() assert calls_a.called - calls_b = mock.MagicMock() + calls_b = MagicMock() mqtt.subscribe(self.hass, "test/state", calls_b) self.hass.block_till_done() assert calls_b.called @@ -639,9 +636,9 @@ class TestMQTTCallbacks(unittest.TestCase): self.hass.block_till_done() expected = [ - mock.call("test/state", 2), - mock.call("test/state", 0), - mock.call("test/state", 1), + call("test/state", 2), + call("test/state", 0), + call("test/state", 1), ] assert self.hass.data["mqtt"]._mqttc.subscribe.mock_calls == expected @@ -653,7 +650,7 @@ class TestMQTTCallbacks(unittest.TestCase): self.hass.data["mqtt"]._mqtt_on_connect(None, None, None, 0) self.hass.block_till_done() - expected.append(mock.call("test/state", 1)) + expected.append(call("test/state", 1)) assert self.hass.data["mqtt"]._mqttc.subscribe.mock_calls == expected @@ -661,9 +658,9 @@ async def test_setup_embedded_starts_with_no_config(hass): """Test setting up embedded server with no config.""" client_config = ("localhost", 1883, "user", "pass", None, "3.1.1") - with mock.patch( + with patch( "homeassistant.components.mqtt.server.async_start", - return_value=mock_coro(return_value=(True, client_config)), + return_value=(True, client_config), ) as _start: await async_mock_mqtt_client(hass, {}) assert _start.call_count == 1 @@ -673,11 +670,10 @@ async def test_setup_embedded_with_embedded(hass): """Test setting up embedded server with no config.""" client_config = ("localhost", 1883, "user", "pass", None, "3.1.1") - with mock.patch( + with patch( "homeassistant.components.mqtt.server.async_start", - return_value=mock_coro(return_value=(True, client_config)), + return_value=(True, client_config), ) as _start: - _start.return_value = mock_coro(return_value=(True, client_config)) await async_mock_mqtt_client(hass, {"embedded": None}) assert _start.call_count == 1 @@ -686,7 +682,7 @@ async def test_setup_fails_if_no_connect_broker(hass): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - with mock.patch("paho.mqtt.client.Client") as mock_client: + with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 1 assert not await mqtt.async_setup_entry(hass, entry) @@ -695,8 +691,8 @@ async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - with mock.patch("paho.mqtt.client.Client") as mock_client: - mock_client().connect = mock.Mock(side_effect=OSError("Connection error")) + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().connect = MagicMock(side_effect=OSError("Connection error")) with pytest.raises(ConfigEntryNotReady): await mqtt.async_setup_entry(hass, entry) @@ -808,7 +804,7 @@ async def test_mqtt_subscribes_topics_on_connect(hass): mqtt.Subscription("still/pending", None, 1), ] - hass.add_job = mock.MagicMock() + hass.add_job = MagicMock() hass.data["mqtt"]._mqtt_on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -825,6 +821,7 @@ async def test_setup_fails_without_config(hass): assert not await async_setup_component(hass, mqtt.DOMAIN, {}) +@pytest.mark.no_fail_on_log_exception async def test_message_callback_exception_gets_logged(hass, caplog): """Test exception raised by message handler.""" await async_mock_mqtt_component(hass) @@ -874,7 +871,7 @@ async def test_dump_service(hass): """Test that we can dump a topic.""" await async_mock_mqtt_component(hass) - mock_open = mock.mock_open() + mopen = mock_open() await hass.services.async_call( "mqtt", "dump", {"topic": "bla/#", "duration": 3}, blocking=True @@ -882,11 +879,11 @@ async def test_dump_service(hass): async_fire_mqtt_message(hass, "bla/1", "test1") async_fire_mqtt_message(hass, "bla/2", "test2") - with mock.patch("homeassistant.components.mqtt.open", mock_open): + with patch("homeassistant.components.mqtt.open", mopen): async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() - writes = mock_open.return_value.write.mock_calls + writes = mopen.return_value.write.mock_calls assert len(writes) == 2 assert writes[0][1][0] == "bla/1,test1\n" assert writes[1][1][0] == "bla/2,test2\n" @@ -1287,7 +1284,7 @@ async def test_debug_info_wildcard(hass, mqtt_mock): ] start_dt = datetime(2019, 1, 1, 0, 0, 0) - with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt async_fire_mqtt_message(hass, "sensor/abc", "123") @@ -1295,7 +1292,15 @@ async def test_debug_info_wildcard(hass, mqtt_mock): assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert { "topic": "sensor/#", - "messages": [{"topic": "sensor/abc", "payload": "123", "time": start_dt}], + "messages": [ + { + "payload": "123", + "qos": 0, + "retain": False, + "time": start_dt, + "topic": "sensor/abc", + } + ], } in debug_info_data["entities"][0]["subscriptions"] @@ -1329,7 +1334,7 @@ async def test_debug_info_filter_same(hass, mqtt_mock): dt1 = datetime(2019, 1, 1, 0, 0, 0) dt2 = datetime(2019, 1, 1, 0, 0, 1) - with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = dt1 async_fire_mqtt_message(hass, "sensor/abc", "123") async_fire_mqtt_message(hass, "sensor/abc", "123") @@ -1342,8 +1347,20 @@ async def test_debug_info_filter_same(hass, mqtt_mock): assert { "topic": "sensor/#", "messages": [ - {"payload": "123", "time": dt1, "topic": "sensor/abc"}, - {"payload": "123", "time": dt2, "topic": "sensor/abc"}, + { + "payload": "123", + "qos": 0, + "retain": False, + "time": dt1, + "topic": "sensor/abc", + }, + { + "payload": "123", + "qos": 0, + "retain": False, + "time": dt2, + "topic": "sensor/abc", + }, ], } == debug_info_data["entities"][0]["subscriptions"][0] @@ -1378,7 +1395,7 @@ async def test_debug_info_same_topic(hass, mqtt_mock): ] start_dt = datetime(2019, 1, 1, 0, 0, 0) - with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) @@ -1386,6 +1403,8 @@ async def test_debug_info_same_topic(hass, mqtt_mock): assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert { "payload": "123", + "qos": 0, + "retain": False, "time": start_dt, "topic": "sensor/status", } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] @@ -1396,6 +1415,66 @@ async def test_debug_info_same_topic(hass, mqtt_mock): await hass.async_block_till_done() start_dt = datetime(2019, 1, 1, 0, 0, 0) - with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) + + +async def test_debug_info_qos_retain(hass, mqtt_mock): + """Test debug info.""" + config = { + "device": {"identifiers": ["helloworld"]}, + "platform": "mqtt", + "name": "test", + "state_topic": "sensor/#", + "unique_id": "veryunique", + } + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 + assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][ + "subscriptions" + ] + + start_dt = datetime(2019, 1, 1, 0, 0, 0) + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: + dt_utcnow.return_value = start_dt + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=False) + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=True) + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=2, retain=False) + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 + assert { + "payload": "123", + "qos": 0, + "retain": False, + "time": start_dt, + "topic": "sensor/abc", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] + assert { + "payload": "123", + "qos": 1, + "retain": True, + "time": start_dt, + "topic": "sensor/abc", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] + assert { + "payload": "123", + "qos": 2, + "retain": False, + "time": start_dt, + "topic": "sensor/abc", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 9e774bfdf1e..b402c23e299 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -2,6 +2,8 @@ from copy import deepcopy import json +import pytest + from homeassistant.components import vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum @@ -612,6 +614,7 @@ async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic#" }' diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 45473d6f448..7cf034ec4e1 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,8 +153,7 @@ light: payload_off: "off" """ -from unittest import mock -from unittest.mock import patch +import pytest from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start @@ -184,11 +183,11 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import call, patch from tests.common import ( MockConfigEntry, assert_setup_component, async_fire_mqtt_message, - mock_coro, ) from tests.components.light import common @@ -673,7 +672,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ) with patch( "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=mock_coro(fake_state), + return_value=fake_state, ): with assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) @@ -716,12 +715,12 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light_rgb/set", "on", 2, False), - mock.call("test_light_rgb/rgb/set", "255,128,0", 2, False), - mock.call("test_light_rgb/brightness/set", 50, 2, False), - mock.call("test_light_rgb/hs/set", "359.0,78.0", 2, False), - mock.call("test_light_rgb/white_value/set", 80, 2, False), - mock.call("test_light_rgb/xy/set", "0.14,0.131", 2, False), + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/rgb/set", "255,128,0", 2, False), + call("test_light_rgb/brightness/set", 50, 2, False), + call("test_light_rgb/hs/set", "359.0,78.0", 2, False), + call("test_light_rgb/white_value/set", 80, 2, False), + call("test_light_rgb/xy/set", "0.14,0.131", 2, False), ], any_order=True, ) @@ -760,8 +759,8 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light_rgb/set", "on", 0, False), - mock.call("test_light_rgb/rgb/set", "#ff803f", 0, False), + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgb/set", "#ff803f", 0, False), ], any_order=True, ) @@ -795,8 +794,8 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light_color_temp/set", "on", 0, False), - mock.call("test_light_color_temp/color_temp/set", "10", 0, False), + call("test_light_color_temp/set", "on", 0, False), + call("test_light_color_temp/color_temp/set", "10", 0, False), ], any_order=True, ) @@ -980,10 +979,9 @@ async def test_on_command_first(hass, mqtt_mock): # test_light/bright: 50 mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light/set", "ON", 0, False), - mock.call("test_light/bright", 50, 0, False), + call("test_light/set", "ON", 0, False), + call("test_light/bright", 50, 0, False), ], - any_order=True, ) mqtt_mock.async_publish.reset_mock() @@ -1015,10 +1013,9 @@ async def test_on_command_last(hass, mqtt_mock): # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light/bright", 50, 0, False), - mock.call("test_light/set", "ON", 0, False), + call("test_light/bright", 50, 0, False), + call("test_light/set", "ON", 0, False), ], - any_order=True, ) mqtt_mock.async_publish.reset_mock() @@ -1066,14 +1063,79 @@ async def test_on_command_brightness(hass, mqtt_mock): await common.async_turn_off(hass, "light.test") - # Turn on w/ just a color to insure brightness gets + # Turn on w/ just a color to ensure brightness gets # added and sent. await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light/rgb", "255,128,0", 0, False), - mock.call("test_light/bright", 50, 0, False), + call("test_light/rgb", "255,128,0", 0, False), + call("test_light/bright", 50, 0, False), + ], + any_order=True, + ) + + +async def test_on_command_brightness_scaled(hass, mqtt_mock): + """Test brightness scale.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "brightness_command_topic": "test_light/bright", + "brightness_scale": 100, + "rgb_command_topic": "test_light/rgb", + "on_command_type": "brightness", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn on w/ no brightness - should set to max + await common.async_turn_on(hass, "light.test") + + # Should get the following MQTT messages. + # test_light/bright: 100 + mqtt_mock.async_publish.assert_called_once_with("test_light/bright", 100, 0, False) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + mqtt_mock.async_publish.reset_mock() + + # Turn on w/ brightness + await common.async_turn_on(hass, "light.test", brightness=50) + + mqtt_mock.async_publish.assert_called_once_with("test_light/bright", 20, 0, False) + mqtt_mock.async_publish.reset_mock() + + # Turn on w/ max brightness + await common.async_turn_on(hass, "light.test", brightness=255) + + mqtt_mock.async_publish.assert_called_once_with("test_light/bright", 100, 0, False) + mqtt_mock.async_publish.reset_mock() + + # Turn on w/ min brightness + await common.async_turn_on(hass, "light.test", brightness=1) + + mqtt_mock.async_publish.assert_called_once_with("test_light/bright", 1, 0, False) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + # Turn on w/ just a color to ensure brightness gets + # added and sent. + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,128,0", 0, False), + call("test_light/bright", 1, 0, False), ], any_order=True, ) @@ -1102,8 +1164,36 @@ async def test_on_command_rgb(hass, mqtt_mock): # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light/rgb", "127,127,127", 0, False), - mock.call("test_light/set", "ON", 0, False), + call("test_light/rgb", "127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgb: '1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,1,1", 0, False), + call("test_light/set", "ON", 0, False), ], any_order=True, ) @@ -1113,6 +1203,32 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,128,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + async def test_on_command_rgb_template(hass, mqtt_mock): """Test on command in RGB brightness mode with RGB template.""" @@ -1138,8 +1254,8 @@ async def test_on_command_rgb_template(hass, mqtt_mock): # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light/rgb", "127/127/127", 0, False), - mock.call("test_light/set", "ON", 0, False), + call("test_light/rgb", "127/127/127", 0, False), + call("test_light/set", "ON", 0, False), ], any_order=True, ) @@ -1174,8 +1290,8 @@ async def test_effect(hass, mqtt_mock): # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ - mock.call("test_light/effect/set", "rainbow", 0, False), - mock.call("test_light/set", "ON", 0, False), + call("test_light/effect/set", "rainbow", 0, False), + call("test_light/set", "ON", 0, False), ], any_order=True, ) @@ -1306,6 +1422,7 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -1366,3 +1483,22 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) + + +async def test_max_mireds(hass, mqtt_mock): + """Test setting min_mireds and max_mireds.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_max_mireds/set", + "color_temp_command_topic": "test_max_mireds/color_temp/set", + "max_mireds": 370, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.attributes.get("min_mireds") == 153 + assert state.attributes.get("max_mireds") == 370 diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 824085ea833..b98282b5288 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -88,8 +88,8 @@ light: brightness_scale: 99 """ import json -from unittest import mock -from unittest.mock import patch + +import pytest from homeassistant.components import light from homeassistant.const import ( @@ -123,7 +123,8 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message, mock_coro +from tests.async_mock import call, patch +from tests.common import async_fire_mqtt_message from tests.components.light import common DEFAULT_CONFIG = { @@ -323,7 +324,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): with patch( "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=mock_coro(fake_state), + return_value=fake_state, ): assert await async_setup_component( hass, @@ -397,7 +398,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 0, "g": 123, "b": 255,' @@ -407,7 +408,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 2, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 255, "g": 56, "b": 59,' @@ -417,7 +418,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 2, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0,' @@ -471,7 +472,7 @@ async def test_sending_hs_color(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"h": 210.824, "s": 100.0},' @@ -480,7 +481,7 @@ async def test_sending_hs_color(hass, mqtt_mock): 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"h": 359.0, "s": 78.0},' @@ -489,7 +490,7 @@ async def test_sending_hs_color(hass, mqtt_mock): 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"h": 30.118, "s": 100.0},' @@ -532,19 +533,19 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call( + call( "test_light_rgb/set", JsonValidator('{"state": "ON", "color": {"r": 0, "g": 24, "b": 50}}'), 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator('{"state": "ON", "color": {"r": 50, "g": 11, "b": 11}}'), 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0}}'), 0, @@ -578,14 +579,15 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) - await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, "light.test", brightness=255, hs_color=[359, 78]) + await common.async_turn_on(hass, "light.test", brightness=1) await common.async_turn_on( hass, "light.test", rgb_color=[255, 128, 0], white_value=80 ) mqtt_mock.async_publish.assert_has_calls( [ - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 0, "g": 123, "b": 255},' @@ -594,16 +596,91 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 255, "g": 56, "b": 59},' - ' "brightness": 50}' + ' "brightness": 255}' ), 0, False, ), - mock.call( + call( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "brightness": 1}'), + 0, + False, + ), + call( + "test_light_rgb/set", + JsonValidator( + '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0},' + ' "white_value": 80}' + ), + 0, + False, + ), + ], + ) + + +async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): + """Test light.turn_on with hs color sends rgb color parameters.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness": True, + "brightness_scale": 100, + "rgb": True, + } + }, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on( + hass, "light.test", brightness=50, xy_color=[0.123, 0.123] + ) + await common.async_turn_on(hass, "light.test", brightness=255, hs_color=[359, 78]) + await common.async_turn_on(hass, "light.test", brightness=1) + await common.async_turn_on( + hass, "light.test", rgb_color=[255, 128, 0], white_value=80 + ) + + mqtt_mock.async_publish.assert_has_calls( + [ + call( + "test_light_rgb/set", + JsonValidator( + '{"state": "ON", "color": {"r": 0, "g": 123, "b": 255},' + ' "brightness": 20}' + ), + 0, + False, + ), + call( + "test_light_rgb/set", + JsonValidator( + '{"state": "ON", "color": {"r": 255, "g": 56, "b": 59},' + ' "brightness": 100}' + ), + 0, + False, + ), + call( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "brightness": 1}'), + 0, + False, + ), + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0},' @@ -613,7 +690,6 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): False, ), ], - any_order=True, ) @@ -647,7 +723,7 @@ async def test_sending_xy_color(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"x": 0.14, "y": 0.131},' @@ -656,7 +732,7 @@ async def test_sending_xy_color(hass, mqtt_mock): 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"x": 0.654, "y": 0.301},' @@ -665,7 +741,7 @@ async def test_sending_xy_color(hass, mqtt_mock): 0, False, ), - mock.call( + call( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"x": 0.611, "y": 0.375},' @@ -1081,6 +1157,7 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -1140,5 +1217,25 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, payload='{"state":"ON"}' ) + + +async def test_max_mireds(hass, mqtt_mock): + """Test setting min_mireds and max_mireds.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "color_temp": True, + "max_mireds": 370, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.attributes.get("min_mireds") == 153 + assert state.attributes.get("max_mireds") == 370 diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 37617192dd5..edb7900e0da 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -26,7 +26,7 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ -from unittest.mock import patch +import pytest from homeassistant.components import light from homeassistant.const import ( @@ -60,7 +60,8 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import assert_setup_component, async_fire_mqtt_message, mock_coro +from tests.async_mock import patch +from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.light import common DEFAULT_CONFIG = { @@ -287,7 +288,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): with patch( "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=mock_coro(fake_state), + return_value=fake_state, ): with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -902,6 +903,7 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' @@ -962,6 +964,37 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" - await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG - ) + config = { + light.DOMAIN: { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test-topic", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + "state_template": '{{ value.split(",")[0] }}', + } + } + await help_test_entity_debug_info_message(hass, mqtt_mock, light.DOMAIN, config) + + +async def test_max_mireds(hass, mqtt_mock): + """Test setting min_mireds and max_mireds.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_max_mireds/set", + "command_on_template": "on", + "command_off_template": "off", + "color_temp_template": "{{ value }}", + "max_mireds": 370, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.attributes.get("min_mireds") == 153 + assert state.attributes.get("max_mireds") == 370 diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index a6e01102152..80ecbde3c4d 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,4 +1,6 @@ """The tests for the MQTT lock platform.""" +import pytest + from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, @@ -366,6 +368,7 @@ async def test_discovery_update_lock(hass, mqtt_mock, caplog): await help_test_discovery_update(hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, data2) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 34d3c33f8d7..58c98f02484 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1,7 +1,8 @@ """The tests for the MQTT sensor platform.""" from datetime import datetime, timedelta import json -from unittest.mock import patch + +import pytest from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start @@ -37,6 +38,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -361,6 +363,7 @@ async def test_discovery_update_sensor(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "state_topic": "test_topic#" }' diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index a9b5656a0b3..b3320d6aaca 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,13 +1,19 @@ """The tests for the MQTT component embedded server.""" from unittest.mock import MagicMock, Mock -from asynctest import CoroutineMock, patch +import pytest import homeassistant.components.mqtt as mqtt from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, mock_coro, mock_storage +from tests.async_mock import AsyncMock, patch +from tests.common import get_test_home_assistant, mock_coro + + +@pytest.fixture(autouse=True) +def inject_fixture(hass_storage): + """Inject pytest fixtures.""" class TestMQTT: @@ -16,25 +22,22 @@ class TestMQTT: def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - self.mock_storage.__exit__(None, None, None) @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=CoroutineMock()))) - @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=AsyncMock()))) + @patch("hbmqtt.broker.Broker.start", AsyncMock(return_value=None)) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): """Test if the MQTT server gets started with password. Since 0.77, MQTT server has to set up its own password. """ - mock_mqtt().async_connect.return_value = mock_coro(True) + mock_mqtt().async_connect = AsyncMock(return_value=True) self.hass.bus.listen_once = MagicMock() password = "mqtt_secret" @@ -48,15 +51,15 @@ class TestMQTT: @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=CoroutineMock()))) - @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=AsyncMock()))) + @patch("hbmqtt.broker.Broker.start", AsyncMock(return_value=None)) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): """Test if the MQTT server gets started with password. Since 0.77, MQTT server has to set up its own password. """ - mock_mqtt().async_connect.return_value = mock_coro(True) + mock_mqtt().async_connect = AsyncMock(return_value=True) self.hass.bus.listen_once = MagicMock() password = "mqtt_secret" diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 367b9ceda8a..15429e6bc57 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -2,6 +2,8 @@ from copy import deepcopy import json +import pytest + from homeassistant.components import vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum @@ -298,6 +300,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 +@pytest.mark.no_fail_on_log_exception async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) @@ -406,6 +409,7 @@ async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "schema": "state", "name": "Beer",' ' "command_topic": "test_topic#"}' @@ -460,5 +464,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, payload="{}" ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 1aaeb154dc2..b51812d2fa3 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,5 +1,4 @@ """The tests for the MQTT switch platform.""" -from asynctest import patch import pytest from homeassistant.components import switch @@ -29,7 +28,8 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message, async_mock_mqtt_component, mock_coro +from tests.async_mock import patch +from tests.common import async_fire_mqtt_message, async_mock_mqtt_component from tests.components.switch import common DEFAULT_CONFIG = { @@ -81,7 +81,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): with patch( "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=mock_coro(fake_state), + return_value=fake_state, ): assert await async_setup_component( hass, @@ -314,6 +314,7 @@ async def test_discovery_update_switch(hass, mqtt_mock, caplog): ) +@pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 36698db87e1..8050535eed4 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -1,6 +1,7 @@ """The tests for the MQTT eventstream component.""" import json -from unittest.mock import ANY, patch + +import pytest import homeassistant.components.mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED @@ -9,30 +10,32 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import ANY, patch from tests.common import ( fire_mqtt_message, fire_time_changed, get_test_home_assistant, mock_mqtt_component, mock_state_change_event, - mock_storage, ) +@pytest.fixture(autouse=True) +def mock_storage(hass_storage): + """Autouse hass_storage for the TestCase tests.""" + + class TestMqttEventStream: """Test the MQTT eventstream module.""" def setup_method(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() self.mock_mqtt = mock_mqtt_component(self.hass) def teardown_method(self): """Stop everything that was started.""" self.hass.stop() - self.mock_storage.__exit__(None, None, None) def add_eventstream(self, sub_topic=None, pub_topic=None, ignore_event=None): """Add a mqtt_eventstream component.""" diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 9efff135fe2..864b3c232ed 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -3,7 +3,6 @@ import json import logging import os -from asynctest import patch import pytest from homeassistant.components.device_tracker.legacy import ( @@ -13,6 +12,7 @@ from homeassistant.components.device_tracker.legacy import ( from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_mock_mqtt_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index b3155e563f4..f11786951f6 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the MQTT room presence sensor.""" import datetime import json -from unittest.mock import patch from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS import homeassistant.components.sensor as sensor @@ -9,6 +8,7 @@ from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.setup import async_setup_component from homeassistant.util import dt +from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_mock_mqtt_component DEVICE_ID = "123TESTMAC" diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index af9a721f1a4..aa9ef0d5de8 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -1,32 +1,34 @@ """The tests for the MQTT statestream component.""" -from unittest.mock import ANY, call, patch +import pytest import homeassistant.components.mqtt_statestream as statestream from homeassistant.core import State from homeassistant.setup import setup_component +from tests.async_mock import ANY, call, patch from tests.common import ( get_test_home_assistant, mock_mqtt_component, mock_state_change_event, - mock_storage, ) +@pytest.fixture(autouse=True) +def mock_storage(hass_storage): + """Autouse hass_storage for the TestCase tests.""" + + class TestMqttStateStream: """Test the MQTT statestream module.""" def setup_method(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() self.mock_mqtt = mock_mqtt_component(self.hass) def teardown_method(self): """Stop everything that was started.""" self.hass.stop() - self.mock_storage.__exit__(None, None, None) def add_statestream( self, diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index 9fb3b34ca63..ed022df0dd7 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -1,11 +1,11 @@ """Test the MyQ config flow.""" -from asynctest import patch from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant import config_entries, setup from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry @@ -111,10 +111,18 @@ async def test_form_homekit(hass): await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "homekit"} + DOMAIN, + context={"source": "homekit"}, + data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "form" assert result["errors"] == {} + flow = next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF" entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} @@ -122,6 +130,8 @@ async def test_form_homekit(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "homekit"} + DOMAIN, + context={"source": "homekit"}, + data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "abort" diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 7cff7bd2af9..61e49a98b83 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -2,12 +2,11 @@ import json -from asynctest import patch - from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index ee037a029ed..e8efac2c01d 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,11 +1,11 @@ """Test the Mythic Beasts DNS component.""" import logging -import asynctest - from homeassistant.components import mythicbeastsdns from homeassistant.setup import async_setup_component +from tests.async_mock import patch + _LOGGER = logging.getLogger(__name__) @@ -20,7 +20,7 @@ async def mbddns_update_mock(domain, password, host, ttl=60, session=None): return True -@asynctest.mock.patch("mbddns.update", new=mbddns_update_mock) +@patch("mbddns.update", new=mbddns_update_mock) async def test_update(hass): """Run with correct values and check true is returned.""" result = await async_setup_component( @@ -37,7 +37,7 @@ async def test_update(hass): assert result -@asynctest.mock.patch("mbddns.update", new=mbddns_update_mock) +@patch("mbddns.update", new=mbddns_update_mock) async def test_update_fails_if_wrong_token(hass): """Run with incorrect token and check false is returned.""" result = await async_setup_component( @@ -54,7 +54,7 @@ async def test_update_fails_if_wrong_token(hass): assert not result -@asynctest.mock.patch("mbddns.update", new=mbddns_update_mock) +@patch("mbddns.update", new=mbddns_update_mock) async def test_update_fails_if_invalid_host(hass): """Run with invalid characters in host and check false is returned.""" result = await async_setup_component( diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 59db79c1052..be69e0853ad 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the Neato config flow.""" -from unittest.mock import patch - from pybotvac.exceptions import NeatoLoginException, NeatoRobotException import pytest @@ -9,6 +7,7 @@ from homeassistant.components.neato import config_flow from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry USERNAME = "myUsername" diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index 8fa6ad05945..182ef98e529 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -1,6 +1,4 @@ """Tests for the Neato init file.""" -from unittest.mock import patch - from pybotvac.exceptions import NeatoLoginException import pytest @@ -8,6 +6,7 @@ from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry USERNAME = "myUsername" diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 9da361852e9..f959b5345dd 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,7 +1,6 @@ """Tests for the ness_alarm component.""" from enum import Enum -from asynctest import MagicMock, patch import pytest from homeassistant.components import alarm_control_panel @@ -32,6 +31,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + VALID_CONFIG = { DOMAIN: { CONF_HOST: "alarm.local", diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index ec6218fb0d7..6c0c0197a74 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant import data_entry_flow from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock from tests.common import mock_coro @@ -33,8 +34,8 @@ async def test_abort_if_already_setup(hass): async def test_full_flow_implementation(hass): """Test registering an implementation and finishing flow works.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) - convert_code = Mock(return_value=mock_coro({"access_token": "yoo"})) + gen_authorize_url = AsyncMock(return_value="https://example.com") + convert_code = AsyncMock(return_value={"access_token": "yoo"}) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code ) @@ -62,7 +63,7 @@ async def test_full_flow_implementation(hass): async def test_not_pick_implementation_if_only_one(hass): """Test we allow picking implementation if we have two.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, None ) @@ -104,7 +105,7 @@ async def test_abort_if_exception_generating_auth_url(hass): async def test_verify_code_timeout(hass): """Test verify code timing out.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=asyncio.TimeoutError) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code @@ -124,7 +125,7 @@ async def test_verify_code_timeout(hass): async def test_verify_code_invalid(hass): """Test verify code invalid.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=config_flow.CodeInvalid) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code @@ -144,7 +145,7 @@ async def test_verify_code_invalid(hass): async def test_verify_code_unknown_error(hass): """Test verify code unknown error.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=config_flow.NestAuthError) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code @@ -164,7 +165,7 @@ async def test_verify_code_unknown_error(hass): async def test_verify_code_exception(hass): """Test verify code blows up.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=ValueError) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 29a1d4f53d5..047dd4c0c40 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,6 +1,4 @@ """Test the Netatmo config flow.""" -from asynctest import patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( @@ -10,6 +8,7 @@ from homeassistant.components.netatmo.const import ( ) from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index ff6f8590287..0dce512cff4 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,12 +1,12 @@ """Test the nexia config flow.""" -from asynctest import patch -from asynctest.mock import MagicMock from requests.exceptions import ConnectTimeout from homeassistant import config_entries, setup from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index cc2b11afcbe..2da56d50f37 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -1,7 +1,6 @@ """Tests for the nexia integration.""" import uuid -from asynctest import patch from nexia.home import NexiaHome import requests_mock @@ -9,6 +8,7 @@ from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index bee9db445e2..dd709618ec0 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy -from unittest.mock import patch import pytest import homeassistant.components.nextbus.sensor as nextbus import homeassistant.components.sensor as sensor +from tests.async_mock import patch from tests.common import assert_setup_component, async_setup_component VALID_AGENCY = "sf-muni" diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 60ca4c07fb5..6aaf7df9505 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,6 +1,4 @@ """Define tests for the Notion config flow.""" -from unittest.mock import patch - import aionotion import pytest @@ -9,20 +7,21 @@ from homeassistant.components.notion import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, patch +from tests.common import MockConfigEntry @pytest.fixture -def mock_client_coro(): +def mock_client(): """Define a fixture for a client creation coroutine.""" - return mock_coro() + return AsyncMock(return_value=None) @pytest.fixture -def mock_aionotion(mock_client_coro): +def mock_aionotion(mock_client): """Mock the aionotion library.""" with patch("homeassistant.components.notion.config_flow.async_get_client") as mock_: - mock_.return_value = mock_client_coro + mock_.side_effect = mock_client yield mock_ @@ -43,7 +42,7 @@ async def test_duplicate_error(hass): @pytest.mark.parametrize( - "mock_client_coro", [mock_coro(exception=aionotion.errors.NotionError)] + "mock_client", [AsyncMock(side_effect=aionotion.errors.NotionError)] ) async def test_invalid_credentials(hass, mock_aionotion): """Test that an invalid API/App Key throws an error.""" diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index 11a3d469a59..2d348204dcc 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -1,10 +1,10 @@ """The tests for the NSW Fuel Station sensor platform.""" import unittest -from unittest.mock import patch from homeassistant.components import sensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant VALID_CONFIG = { diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index 6834c557bd5..584616967c4 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,9 +1,7 @@ """The tests for the NSW Rural Fire Service Feeds platform.""" import datetime -from unittest.mock import ANY from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed -from asynctest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -37,6 +35,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import ANY, MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py index a9adfd3aa57..9755335ccc1 100644 --- a/tests/components/nuheat/mocks.py +++ b/tests/components/nuheat/mocks.py @@ -1,10 +1,11 @@ """The test for the NuHeat thermostat module.""" -from asynctest.mock import MagicMock, Mock from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD from homeassistant.components.nuheat.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, Mock + def _get_mock_thermostat_run(): serial_number = "12345" diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 7bf52026ef9..b407461fa89 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -1,6 +1,4 @@ """The test for the NuHeat thermostat module.""" -from asynctest.mock import patch - from homeassistant.components.nuheat.const import DOMAIN from homeassistant.setup import async_setup_component @@ -13,6 +11,8 @@ from .mocks import ( _mock_get_config, ) +from tests.async_mock import patch + async def test_climate_thermostat_run(hass): """Test a thermostat with the schedule running.""" diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 338509f09d1..4c392841142 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,5 +1,4 @@ """Test the NuHeat config flow.""" -from asynctest import MagicMock, patch import requests from homeassistant import config_entries, setup @@ -8,6 +7,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERV from .mocks import _get_mock_thermostat_run +from tests.async_mock import MagicMock, patch + async def test_form_user(hass): """Test we get the form with user source.""" diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index 01128610462..4a7a8673230 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,11 +1,11 @@ """NuHeat component tests.""" -from unittest.mock import patch - from homeassistant.components.nuheat.const import DOMAIN from homeassistant.setup import async_setup_component from .mocks import _get_mock_nuheat +from tests.async_mock import patch + VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} } diff --git a/tests/components/numato/__init__.py b/tests/components/numato/__init__.py new file mode 100644 index 00000000000..bcef00f9594 --- /dev/null +++ b/tests/components/numato/__init__.py @@ -0,0 +1 @@ +"""Tests for the numato integration.""" diff --git a/tests/components/numato/common.py b/tests/components/numato/common.py new file mode 100644 index 00000000000..18ece6690bc --- /dev/null +++ b/tests/components/numato/common.py @@ -0,0 +1,49 @@ +"""Definitions shared by all numato tests.""" + +from numato_gpio import NumatoGpioError + +NUMATO_CFG = { + "numato": { + "discover": ["/ttyACM0", "/ttyACM1"], + "devices": [ + { + "id": 0, + "binary_sensors": { + "invert_logic": False, + "ports": { + "2": "numato_binary_sensor_mock_port2", + "3": "numato_binary_sensor_mock_port3", + "4": "numato_binary_sensor_mock_port4", + }, + }, + "sensors": { + "ports": { + "1": { + "name": "numato_adc_mock_port1", + "source_range": [100, 1023], + "destination_range": [0, 10], + "unit": "mocks", + } + }, + }, + "switches": { + "invert_logic": False, + "ports": { + "5": "numato_switch_mock_port5", + "6": "numato_switch_mock_port6", + }, + }, + } + ], + } +} + + +def mockup_raise(*args, **kwargs): + """Mockup to replace regular functions for error injection.""" + raise NumatoGpioError("Error mockup") + + +def mockup_return(*args, **kwargs): + """Mockup to replace regular functions for error injection.""" + return False diff --git a/tests/components/numato/conftest.py b/tests/components/numato/conftest.py new file mode 100644 index 00000000000..c6fd13a099e --- /dev/null +++ b/tests/components/numato/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for numato tests.""" + +from copy import deepcopy + +import pytest + +from homeassistant.components import numato + +from . import numato_mock +from .common import NUMATO_CFG + + +@pytest.fixture +def config(): + """Provide a copy of the numato domain's test configuration. + + This helps to quickly change certain aspects of the configuration scoped + to each individual test. + """ + return deepcopy(NUMATO_CFG) + + +@pytest.fixture +def numato_fixture(monkeypatch): + """Inject the numato mockup into numato homeassistant module.""" + module_mock = numato_mock.NumatoModuleMock() + monkeypatch.setattr(numato, "gpio", module_mock) + return module_mock diff --git a/tests/components/numato/numato_mock.py b/tests/components/numato/numato_mock.py new file mode 100644 index 00000000000..1f8b24027de --- /dev/null +++ b/tests/components/numato/numato_mock.py @@ -0,0 +1,68 @@ +"""Mockup for the numato component interface.""" +from numato_gpio import NumatoGpioError + + +class NumatoModuleMock: + """Mockup for the numato_gpio module.""" + + NumatoGpioError = NumatoGpioError + + def __init__(self): + """Initialize the numato_gpio module mockup class.""" + self.devices = {} + + class NumatoDeviceMock: + """Mockup for the numato_gpio.NumatoUsbGpio class.""" + + def __init__(self, device): + """Initialize numato device mockup.""" + self.device = device + self.callbacks = {} + self.ports = set() + self.values = {} + + def setup(self, port, direction): + """Mockup for setup.""" + self.ports.add(port) + self.values[port] = None + + def write(self, port, value): + """Mockup for write.""" + self.values[port] = value + + def read(self, port): + """Mockup for read.""" + return 1 + + def adc_read(self, port): + """Mockup for adc_read.""" + return 1023 + + def add_event_detect(self, port, callback, direction): + """Mockup for add_event_detect.""" + self.callbacks[port] = callback + + def notify(self, enable): + """Mockup for notify.""" + + def mockup_inject_notification(self, port, value): + """Make the mockup execute a notification callback.""" + self.callbacks[port](port, value) + + OUT = 0 + IN = 1 + + RISING = 1 + FALLING = 2 + BOTH = 3 + + def discover(self, _=None): + """Mockup for the numato device discovery. + + Ignore the device list argument, mock discovers /dev/ttyACM0. + """ + self.devices[0] = NumatoModuleMock.NumatoDeviceMock("/dev/ttyACM0") + + def cleanup(self): + """Mockup for the numato device cleanup.""" + self.devices.clear() diff --git a/tests/components/numato/test_binary_sensor.py b/tests/components/numato/test_binary_sensor.py new file mode 100644 index 00000000000..5aa6aea2b8d --- /dev/null +++ b/tests/components/numato/test_binary_sensor.py @@ -0,0 +1,62 @@ +"""Tests for the numato binary_sensor platform.""" +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise + +MOCKUP_ENTITY_IDS = { + "binary_sensor.numato_binary_sensor_mock_port2", + "binary_sensor.numato_binary_sensor_mock_port3", + "binary_sensor.numato_binary_sensor_mock_port4", +} + + +async def test_failing_setups_no_entities(hass, numato_fixture, monkeypatch): + """When port setup fails, no entity shall be created.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + + +async def test_setup_callbacks(hass, numato_fixture, monkeypatch): + """During setup a callback shall be registered.""" + + numato_fixture.discover() + + def mock_add_event_detect(self, port, callback, direction): + assert self == numato_fixture.devices[0] + assert port == 1 + assert callback is callable + assert direction == numato_fixture.BOTH + + monkeypatch.setattr( + numato_fixture.devices[0], "add_event_detect", mock_add_event_detect + ) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + + +async def test_hass_binary_sensor_notification(hass, numato_fixture): + """Test regular operations from within Home Assistant.""" + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() # wait until services are registered + assert ( + hass.states.get("binary_sensor.numato_binary_sensor_mock_port2").state == "on" + ) + await hass.async_add_executor_job(numato_fixture.devices[0].callbacks[2], 2, False) + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.numato_binary_sensor_mock_port2").state == "off" + ) + + +async def test_binary_sensor_setup_without_discovery_info(hass, config, numato_fixture): + """Test handling of empty discovery_info.""" + numato_fixture.discover() + await discovery.async_load_platform(hass, "binary_sensor", "numato", None, config) + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + await hass.async_block_till_done() # wait for numato platform to be loaded + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id in hass.states.async_entity_ids() diff --git a/tests/components/numato/test_init.py b/tests/components/numato/test_init.py new file mode 100644 index 00000000000..dd5643be12a --- /dev/null +++ b/tests/components/numato/test_init.py @@ -0,0 +1,161 @@ +"""Tests for the numato integration.""" +from numato_gpio import NumatoGpioError +import pytest + +from homeassistant.components import numato +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise, mockup_return + + +async def test_setup_no_devices(hass, numato_fixture, monkeypatch): + """Test handling of an 'empty' discovery. + + Platform setups are expected to return after handling errors locally + without raising. + """ + monkeypatch.setattr(numato_fixture, "discover", mockup_return) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + assert len(numato_fixture.devices) == 0 + + +async def test_fail_setup_raising_discovery(hass, numato_fixture, caplog, monkeypatch): + """Test handling of an exception during discovery. + + Setup shall return False. + """ + monkeypatch.setattr(numato_fixture, "discover", mockup_raise) + assert not await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + + +async def test_hass_numato_api_wrong_port_directions(hass, numato_fixture): + """Test handling of wrong port directions. + + This won't happen in the current platform implementation but would raise + in case of an introduced bug in the platforms. + """ + numato_fixture.discover() + api = numato.NumatoAPI() + api.setup_output(0, 5) + api.setup_input(0, 2) + api.setup_input(0, 6) + with pytest.raises(NumatoGpioError): + api.read_adc_input(0, 5) # adc_read from output + api.read_input(0, 6) # read from output + api.write_output(0, 2, 1) # write to input + + +async def test_hass_numato_api_errors(hass, numato_fixture, monkeypatch): + """Test whether Home Assistant numato API (re-)raises errors.""" + numato_fixture.discover() + monkeypatch.setattr(numato_fixture.devices[0], "setup", mockup_raise) + monkeypatch.setattr(numato_fixture.devices[0], "adc_read", mockup_raise) + monkeypatch.setattr(numato_fixture.devices[0], "read", mockup_raise) + monkeypatch.setattr(numato_fixture.devices[0], "write", mockup_raise) + api = numato.NumatoAPI() + with pytest.raises(NumatoGpioError): + api.setup_input(0, 5) + api.read_adc_input(0, 1) + api.read_input(0, 2) + api.write_output(0, 2, 1) + + +async def test_invalid_port_number(hass, numato_fixture, config): + """Test validation of ADC port number type.""" + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + port1_config = sensorports_cfg["1"] + sensorports_cfg["one"] = port1_config + del sensorports_cfg["1"] + assert not await async_setup_component(hass, "numato", config) + await hass.async_block_till_done() + assert not numato_fixture.devices + + +async def test_too_low_adc_port_number(hass, numato_fixture, config): + """Test handling of failing component setup. + + Tries setting up an ADC on a port below (0) the allowed range. + """ + + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg.update({0: {"name": "toolow"}}) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_too_high_adc_port_number(hass, numato_fixture, config): + """Test handling of failing component setup. + + Tries setting up an ADC on a port above (8) the allowed range. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg.update({8: {"name": "toohigh"}}) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_range_value_type(hass, numato_fixture, config): + """Test validation of ADC range config's types. + + Replaces the source range beginning by a string. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["source_range"][0] = "zero" + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_source_range_length(hass, numato_fixture, config): + """Test validation of ADC range config's length. + + Adds an element to the source range. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["source_range"].append(42) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_source_range_order(hass, numato_fixture, config): + """Test validation of ADC range config's order. + + Sets the source range to a decreasing [2, 1]. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["source_range"] = [2, 1] + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_destination_range_value_type(hass, numato_fixture, config): + """Test validation of ADC range . + + Replaces the destination range beginning by a string. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["destination_range"][0] = "zero" + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_destination_range_length(hass, numato_fixture, config): + """Test validation of ADC range config's length. + + Adds an element to the destination range. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["destination_range"].append(42) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_destination_range_order(hass, numato_fixture, config): + """Test validation of ADC range config's order. + + Sets the destination range to a decreasing [2, 1]. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["destination_range"] = [2, 1] + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices diff --git a/tests/components/numato/test_sensor.py b/tests/components/numato/test_sensor.py new file mode 100644 index 00000000000..c6d176dbc90 --- /dev/null +++ b/tests/components/numato/test_sensor.py @@ -0,0 +1,38 @@ +"""Tests for the numato sensor platform.""" +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise + +MOCKUP_ENTITY_IDS = { + "sensor.numato_adc_mock_port1", +} + + +async def test_failing_setups_no_entities(hass, numato_fixture, monkeypatch): + """When port setup fails, no entity shall be created.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + + +async def test_failing_sensor_update(hass, numato_fixture, monkeypatch): + """Test condition when a sensor update fails.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "adc_read", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + assert hass.states.get("sensor.numato_adc_mock_port1").state is STATE_UNKNOWN + + +async def test_sensor_setup_without_discovery_info(hass, config, numato_fixture): + """Test handling of empty discovery_info.""" + numato_fixture.discover() + await discovery.async_load_platform(hass, "sensor", "numato", None, config) + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + await hass.async_block_till_done() # wait for numato platform to be loaded + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id in hass.states.async_entity_ids() diff --git a/tests/components/numato/test_switch.py b/tests/components/numato/test_switch.py new file mode 100644 index 00000000000..91cda5c2a37 --- /dev/null +++ b/tests/components/numato/test_switch.py @@ -0,0 +1,114 @@ +"""Tests for the numato switch platform.""" +from homeassistant.components import switch +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise + +MOCKUP_ENTITY_IDS = { + "switch.numato_switch_mock_port5", + "switch.numato_switch_mock_port6", +} + + +async def test_failing_setups_no_entities(hass, numato_fixture, monkeypatch): + """When port setup fails, no entity shall be created.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + + +async def test_regular_hass_operations(hass, numato_fixture): + """Test regular operations from within Home Assistant.""" + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() # wait until services are registered + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "on" + assert numato_fixture.devices[0].values[5] == 1 + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "on" + assert numato_fixture.devices[0].values[6] == 1 + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "off" + assert numato_fixture.devices[0].values[5] == 0 + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "off" + assert numato_fixture.devices[0].values[6] == 0 + + +async def test_failing_hass_operations(hass, numato_fixture, monkeypatch): + """Test failing operations called from within Home Assistant. + + Switches remain in their initial 'off' state when the device can't + be written to. + """ + assert await async_setup_component(hass, "numato", NUMATO_CFG) + + await hass.async_block_till_done() # wait until services are registered + monkeypatch.setattr(numato_fixture.devices[0], "write", mockup_raise) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "off" + assert not numato_fixture.devices[0].values[5] + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "off" + assert not numato_fixture.devices[0].values[6] + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "off" + assert not numato_fixture.devices[0].values[5] + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "off" + assert not numato_fixture.devices[0].values[6] + + +async def test_switch_setup_without_discovery_info(hass, config, numato_fixture): + """Test handling of empty discovery_info.""" + numato_fixture.discover() + await discovery.async_load_platform(hass, "switch", "numato", None, config) + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + await hass.async_block_till_done() # wait for numato platform to be loaded + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id in hass.states.async_entity_ids() diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index ac20c989de9..7eb0ac20184 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -1,12 +1,11 @@ """Test the Network UPS Tools (NUT) config flow.""" -from asynctest import patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_RESOURCES, CONF_SCAN_INTERVAL from .util import _get_mock_pynutclient +from tests.async_mock import patch from tests.common import MockConfigEntry VALID_CONFIG = { diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 788076a7c9f..5622438d70b 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -2,12 +2,11 @@ import json -from asynctest import MagicMock, patch - from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES from homeassistant.core import HomeAssistant +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 14cee7aa7cb..74f84eb200c 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -1,9 +1,7 @@ """Fixtures for National Weather Service tests.""" -from unittest.mock import patch - import pytest -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch from tests.components.nws.const import DEFAULT_FORECAST, DEFAULT_OBSERVATION @@ -12,10 +10,10 @@ def mock_simple_nws(): """Mock pynws SimpleNWS with default values.""" with patch("homeassistant.components.nws.SimpleNWS") as mock_nws: instance = mock_nws.return_value - instance.set_station.return_value = mock_coro() - instance.update_observation.return_value = mock_coro() - instance.update_forecast.return_value = mock_coro() - instance.update_forecast_hourly.return_value = mock_coro() + instance.set_station = AsyncMock(return_value=None) + instance.update_observation = AsyncMock(return_value=None) + instance.update_forecast = AsyncMock(return_value=None) + instance.update_forecast_hourly = AsyncMock(return_value=None) instance.station = "ABC" instance.stations = ["ABC"] instance.observation = DEFAULT_OBSERVATION @@ -29,7 +27,7 @@ def mock_simple_nws_config(): """Mock pynws SimpleNWS with default values in config_flow.""" with patch("homeassistant.components.nws.config_flow.SimpleNWS") as mock_nws: instance = mock_nws.return_value - instance.set_station.return_value = mock_coro() + instance.set_station = AsyncMock(return_value=None) instance.station = "ABC" instance.stations = ["ABC"] yield mock_nws diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index d4957d4c989..bca852fa379 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -1,10 +1,11 @@ """Test the National Weather Service (NWS) config flow.""" import aiohttp -from asynctest import patch from homeassistant import config_entries, setup from homeassistant.components.nws.const import DOMAIN +from tests.async_mock import patch + async def test_form(hass, mock_simple_nws_config): """Test we get the form.""" diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index f1054104a4a..667f40db137 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components import nws from homeassistant.components.weather import ATTR_FORECAST +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -125,6 +126,32 @@ async def test_error_station(hass, mock_simple_nws): assert hass.states.get("weather.abc_daynight") is None +async def test_entity_refresh(hass, mock_simple_nws): + """Test manual refresh.""" + instance = mock_simple_nws.return_value + + await async_setup_component(hass, "homeassistant", {}) + + entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + instance.update_observation.assert_called_once() + instance.update_forecast.assert_called_once() + instance.update_forecast_hourly.assert_called_once() + + await hass.services.async_call( + "homeassistant", + "update_entity", + {"entity_id": "weather.abc_daynight"}, + blocking=True, + ) + await hass.async_block_till_done() + assert instance.update_observation.call_count == 2 + assert instance.update_forecast.call_count == 2 + instance.update_forecast_hourly.assert_called_once() + + async def test_error_observation(hass, mock_simple_nws): """Test error during update observation.""" instance = mock_simple_nws.return_value diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 91ed8d7ae5c..7deda0e7edc 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,6 +1,5 @@ """Test the onboarding views.""" import asyncio -from unittest.mock import patch import pytest @@ -11,6 +10,7 @@ from homeassistant.setup import async_setup_component from . import mock_storage +from tests.async_mock import patch from tests.common import CLIENT_ID, register_auth_provider from tests.components.met.conftest import mock_weather # noqa: F401 diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py new file mode 100644 index 00000000000..433a6392f12 --- /dev/null +++ b/tests/components/onvif/__init__.py @@ -0,0 +1 @@ +"""Tests for the ONVIF integration.""" diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py new file mode 100644 index 00000000000..c709c5e6f67 --- /dev/null +++ b/tests/components/onvif/test_config_flow.py @@ -0,0 +1,512 @@ +"""Test ONVIF config flow.""" +from asyncio import Future + +from asynctest import MagicMock, patch +from onvif.exceptions import ONVIFError +from zeep.exceptions import Fault + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.onvif import config_flow + +from tests.common import MockConfigEntry + +URN = "urn:uuid:123456789" +NAME = "TestCamera" +HOST = "1.2.3.4" +PORT = 80 +USERNAME = "admin" +PASSWORD = "12345" +MAC = "aa:bb:cc:dd:ee" + +DISCOVERY = [ + { + "EPR": URN, + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + "MAC": MAC, + }, + { + "EPR": "urn:uuid:987654321", + config_flow.CONF_NAME: "TestCamera2", + config_flow.CONF_HOST: "5.6.7.8", + config_flow.CONF_PORT: PORT, + "MAC": "ee:dd:cc:bb:aa", + }, +] + + +def setup_mock_onvif_camera( + mock_onvif_camera, with_h264=True, two_profiles=False, with_interfaces=True +): + """Prepare mock onvif.ONVIFCamera.""" + devicemgmt = MagicMock() + + interface = MagicMock() + interface.Enabled = True + interface.Info.HwAddress = MAC + + devicemgmt.GetNetworkInterfaces.return_value = Future() + devicemgmt.GetNetworkInterfaces.return_value.set_result( + [interface] if with_interfaces else [] + ) + + media_service = MagicMock() + + profile1 = MagicMock() + profile1.VideoEncoderConfiguration.Encoding = "H264" if with_h264 else "MJPEG" + profile2 = MagicMock() + profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG" + + media_service.GetProfiles.return_value = Future() + media_service.GetProfiles.return_value.set_result([profile1, profile2]) + + mock_onvif_camera.update_xaddrs.return_value = Future() + mock_onvif_camera.update_xaddrs.return_value.set_result(True) + mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt) + mock_onvif_camera.create_media_service = MagicMock(return_value=media_service) + + def mock_constructor( + host, + port, + user, + passwd, + wsdl_dir, + encrypt=True, + no_cache=False, + adjust_time=False, + transport=None, + ): + """Fake the controller constructor.""" + return mock_onvif_camera + + mock_onvif_camera.side_effect = mock_constructor + + +def setup_mock_discovery( + mock_discovery, with_name=False, with_mac=False, two_devices=False +): + """Prepare mock discovery result.""" + services = [] + for item in DISCOVERY: + service = MagicMock() + service.getXAddrs = MagicMock( + return_value=[ + f"http://{item[config_flow.CONF_HOST]}:{item[config_flow.CONF_PORT]}/onvif/device_service" + ] + ) + service.getEPR = MagicMock(return_value=item["EPR"]) + scopes = [] + if with_name: + scope = MagicMock() + scope.getValue = MagicMock( + return_value=f"onvif://www.onvif.org/name/{item[config_flow.CONF_NAME]}" + ) + scopes.append(scope) + if with_mac: + scope = MagicMock() + scope.getValue = MagicMock( + return_value=f"onvif://www.onvif.org/mac/{item['MAC']}" + ) + scopes.append(scope) + service.getScopes = MagicMock(return_value=scopes) + services.append(service) + mock_discovery.return_value = services + + +def setup_mock_device(mock_device): + """Prepare mock ONVIFDevice.""" + mock_device.async_setup.return_value = Future() + mock_device.async_setup.return_value.set_result(True) + + def mock_constructor(hass, config): + """Fake the controller constructor.""" + return mock_device + + mock_device.side_effect = mock_constructor + + +async def setup_onvif_integration( + hass, config=None, options=None, unique_id=MAC, entry_id="1", source="user", +): + """Create an ONVIF config entry.""" + if not config: + config = { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + } + + config_entry = MockConfigEntry( + domain=config_flow.DOMAIN, + source=source, + data={**config}, + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + options=options or {}, + entry_id=entry_id, + unique_id=unique_id, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) + # no discovery + mock_discovery.return_value = [] + setup_mock_device(mock_device) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry + + +async def test_flow_discovered_devices(hass): + """Test that config flow works for discovered devices.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera) + setup_mock_discovery(mock_discovery) + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device" + assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 3 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={config_flow.CONF_HOST: f"{URN} ({HOST})"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{URN} - {MAC}" + assert result["data"] == { + config_flow.CONF_NAME: URN, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + } + + +async def test_flow_discovered_devices_ignore_configured_manual_input(hass): + """Test that config flow discovery ignores configured devices.""" + await setup_onvif_integration(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera) + setup_mock_discovery(mock_discovery, with_mac=True) + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device" + assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "manual_input" + + +async def test_flow_discovery_ignore_existing_and_abort(hass): + """Test that config flow discovery ignores setup devices.""" + await setup_onvif_integration(hass) + await setup_onvif_integration( + hass, + config={ + config_flow.CONF_NAME: DISCOVERY[1]["EPR"], + config_flow.CONF_HOST: DISCOVERY[1][config_flow.CONF_HOST], + config_flow.CONF_PORT: DISCOVERY[1][config_flow.CONF_PORT], + config_flow.CONF_USERNAME: "", + config_flow.CONF_PASSWORD: "", + }, + unique_id=DISCOVERY[1]["MAC"], + entry_id="2", + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera) + setup_mock_discovery(mock_discovery, with_name=True, with_mac=True) + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + # It should skip to manual entry if the only devices are already configured + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "manual_input" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + # It should abort if already configured and entered manually + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_flow_manual_entry(hass): + """Test that config flow works for discovered devices.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) + # no discovery + mock_discovery.return_value = [] + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "manual_input" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{NAME} - {MAC}" + assert result["data"] == { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + } + + +async def test_flow_import_no_mac(hass): + """Test that config flow fails when no MAC available.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera: + setup_mock_onvif_camera(mock_onvif_camera, with_interfaces=False) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_mac" + + +async def test_flow_import_no_h264(hass): + """Test that config flow fails when no MAC available.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera: + setup_mock_onvif_camera(mock_onvif_camera, with_h264=False) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_h264" + + +async def test_flow_import_onvif_api_error(hass): + """Test that config flow fails when ONVIF API fails.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera: + setup_mock_onvif_camera(mock_onvif_camera) + mock_onvif_camera.create_devicemgmt_service = MagicMock( + side_effect=ONVIFError("Could not get device mgmt service") + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "onvif_error" + + +async def test_flow_import_onvif_auth_error(hass): + """Test that config flow fails when ONVIF API fails.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera: + setup_mock_onvif_camera(mock_onvif_camera) + mock_onvif_camera.create_devicemgmt_service = MagicMock( + side_effect=Fault("Auth Error") + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + assert result["errors"]["base"] == "connection_failed" + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = await setup_onvif_integration(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "onvif_devices" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_EXTRA_ARGUMENTS: "", + config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + config_flow.CONF_EXTRA_ARGUMENTS: "", + config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + } diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index 4aec9e68709..c164f2f03a2 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,18 +1,13 @@ """The tests for the openalpr cloud platform.""" import asyncio -from unittest.mock import PropertyMock, patch from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - load_fixture, - mock_coro, -) +from tests.async_mock import PropertyMock, patch +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common @@ -145,7 +140,7 @@ class TestOpenAlprCloud: with patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"image")), + return_value=camera.Image("image/jpeg", b"image"), ): common.scan(self.hass, entity_id="image_processing.test_local") self.hass.block_till_done() @@ -178,7 +173,7 @@ class TestOpenAlprCloud: with patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"image")), + return_value=camera.Image("image/jpeg", b"image"), ): common.scan(self.hass, entity_id="image_processing.test_local") self.hass.block_till_done() @@ -194,7 +189,7 @@ class TestOpenAlprCloud: with patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"image")), + return_value=camera.Image("image/jpeg", b"image"), ): common.scan(self.hass, entity_id="image_processing.test_local") self.hass.block_till_done() diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index f28ee6f02d4..996d23184a2 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -1,16 +1,15 @@ """The tests for the openalpr local platform.""" -from unittest.mock import MagicMock, PropertyMock, patch - import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import callback from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common -async def mock_async_subprocess(): +def mock_async_subprocess(): """Get a Popen mock back.""" async_popen = MagicMock() diff --git a/tests/components/openerz/test_sensor.py b/tests/components/openerz/test_sensor.py index 24a0f0610af..e616ea4fe4e 100644 --- a/tests/components/openerz/test_sensor.py +++ b/tests/components/openerz/test_sensor.py @@ -1,9 +1,9 @@ """Tests for OpenERZ component.""" -from unittest.mock import MagicMock, patch - from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + MOCK_CONFIG = { "sensor": { "platform": "openerz", diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index f57ad20f5d5..003d2ad7170 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,6 +1,5 @@ """Test the Opentherm Gateway config flow.""" import asyncio -from unittest.mock import patch from pyotgw.vars import OTGW_ABOUT from serial import SerialException @@ -13,7 +12,8 @@ from homeassistant.components.opentherm_gw.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_form_user(hass): @@ -26,16 +26,13 @@ async def test_form_user(hass): assert result["errors"] == {} with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup", return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} @@ -59,16 +56,13 @@ async def test_form_import(hass): """Test import from existing config.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup", return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -102,16 +96,13 @@ async def test_form_duplicate_entries(hass): ) with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup", return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: result1 = await hass.config_entries.flow.async_configure( flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index ae999afe305..3e6101e1989 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,16 +1,16 @@ """Tests for OwnTracks config flow.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components.owntracks import config_flow from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET from homeassistant.components.owntracks.const import DOMAIN +from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry CONF_WEBHOOK_URL = "webhook_url" @@ -47,9 +47,11 @@ def mock_not_supports_encryption(): yield -def init_config_flow(hass): +async def init_config_flow(hass): """Init a configuration flow.""" - hass.config.api = Mock(base_url=BASE_URL) + await async_process_ha_core_config( + hass, {"external_url": BASE_URL}, + ) flow = config_flow.OwnTracksFlow() flow.hass = hass return flow @@ -57,7 +59,7 @@ def init_config_flow(hass): async def test_user(hass, webhook_id, secret): """Test user step.""" - flow = init_config_flow(hass) + flow = await init_config_flow(hass) result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -74,7 +76,7 @@ async def test_user(hass, webhook_id, secret): async def test_import(hass, webhook_id, secret): """Test import step.""" - flow = init_config_flow(hass) + flow = await init_config_flow(hass) result = await flow.async_step_import({}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -87,6 +89,10 @@ async def test_import(hass, webhook_id, secret): async def test_import_setup(hass): """Test that we automatically create a config flow.""" + await async_process_ha_core_config( + hass, {"external_url": "http://example.com"}, + ) + assert not hass.config_entries.async_entries(DOMAIN) assert await async_setup_component(hass, DOMAIN, {"owntracks": {}}) await hass.async_block_till_done() @@ -95,7 +101,7 @@ async def test_import_setup(hass): async def test_abort_if_already_setup(hass): """Test that we can't add more than one instance.""" - flow = init_config_flow(hass) + flow = await init_config_flow(hass) MockConfigEntry(domain=DOMAIN, data={}).add_to_hass(hass) assert hass.config_entries.async_entries(DOMAIN) @@ -113,7 +119,7 @@ async def test_abort_if_already_setup(hass): async def test_user_not_supports_encryption(hass, not_supports_encryption): """Test user step.""" - flow = init_config_flow(hass) + flow = await init_config_flow(hass) result = await flow.async_step_user({}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -125,6 +131,10 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): async def test_unload(hass): """Test unloading a config flow.""" + await async_process_ha_core_config( + hass, {"external_url": "http://example.com"}, + ) + with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" ) as mock_forward: @@ -141,7 +151,7 @@ async def test_unload(hass): with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload", - return_value=mock_coro(), + return_value=None, ) as mock_unload: assert await hass.config_entries.async_unload(entry.entry_id) @@ -158,7 +168,7 @@ async def test_with_cloud_sub(hass): "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( "homeassistant.components.cloud.async_create_cloudhook", - return_value=mock_coro("https://hooks.nabu.casa/ABCD"), + return_value="https://hooks.nabu.casa/ABCD", ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data={} diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index bdd1199008c..d71f0fe0aee 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,13 +1,13 @@ """The tests for the Owntracks device tracker.""" import json -from asynctest import patch import pytest from homeassistant.components import owntracks from homeassistant.const import STATE_NOT_HOME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py index 2c06ac0c4e7..6d5139caa14 100644 --- a/tests/components/owntracks/test_helper.py +++ b/tests/components/owntracks/test_helper.py @@ -1,10 +1,10 @@ """Test the owntracks_http platform.""" -from unittest.mock import patch - import pytest from homeassistant.components.owntracks import helper +from tests.async_mock import patch + @pytest.fixture(name="nacl_imported") def mock_nacl_imported(): diff --git a/tests/components/ozw/__init__.py b/tests/components/ozw/__init__.py new file mode 100644 index 00000000000..ce419b9f55b --- /dev/null +++ b/tests/components/ozw/__init__.py @@ -0,0 +1 @@ +"""Tests for the OZW integration.""" diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py new file mode 100644 index 00000000000..a71103fdf85 --- /dev/null +++ b/tests/components/ozw/common.py @@ -0,0 +1,57 @@ +"""Helpers for tests.""" +import json + +from homeassistant import config_entries +from homeassistant.components.ozw.const import DOMAIN + +from tests.async_mock import Mock, patch +from tests.common import MockConfigEntry + + +async def setup_ozw(hass, entry=None, fixture=None): + """Set up OZW and load a dump.""" + hass.config.components.add("mqtt") + + if entry is None: + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + ) + + entry.add_to_hass(hass) + + with patch("homeassistant.components.mqtt.async_subscribe") as mock_subscribe: + mock_subscribe.return_value = Mock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "ozw" in hass.config.components + assert len(mock_subscribe.mock_calls) == 1 + receive_message = mock_subscribe.mock_calls[0][1][2] + + if fixture is not None: + for line in fixture.split("\n"): + topic, payload = line.strip().split(",", 1) + receive_message(Mock(topic=topic, payload=payload)) + + await hass.async_block_till_done() + + return receive_message + + +class MQTTMessage: + """Represent a mock MQTT message.""" + + def __init__(self, topic, payload): + """Set up message.""" + self.topic = topic + self.payload = payload + + def decode(self): + """Decode message payload from a string to a json dict.""" + self.payload = json.loads(self.payload) + + def encode(self): + """Encode message payload into a string.""" + self.payload = json.dumps(self.payload) diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py new file mode 100644 index 00000000000..b984172d355 --- /dev/null +++ b/tests/components/ozw/conftest.py @@ -0,0 +1,90 @@ +"""Helpers for tests.""" +import json + +import pytest + +from .common import MQTTMessage + +from tests.async_mock import patch +from tests.common import load_fixture + + +@pytest.fixture(name="generic_data", scope="session") +def generic_data_fixture(): + """Load generic MQTT data and return it.""" + return load_fixture("ozw/generic_network_dump.csv") + + +@pytest.fixture(name="light_data", scope="session") +def light_data_fixture(): + """Load light dimmer MQTT data and return it.""" + return load_fixture("ozw/light_network_dump.csv") + + +@pytest.fixture(name="sent_messages") +def sent_messages_fixture(): + """Fixture to capture sent messages.""" + sent_messages = [] + + with patch( + "homeassistant.components.mqtt.async_publish", + side_effect=lambda hass, topic, payload: sent_messages.append( + {"topic": topic, "payload": json.loads(payload)} + ), + ): + yield sent_messages + + +@pytest.fixture(name="light_msg") +async def light_msg_fixture(hass): + """Return a mock MQTT msg with a light actuator message.""" + light_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/light.json") + ) + message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) + message.encode() + return message + + +@pytest.fixture(name="switch_msg") +async def switch_msg_fixture(hass): + """Return a mock MQTT msg with a switch actuator message.""" + switch_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/switch.json") + ) + message = MQTTMessage(topic=switch_json["topic"], payload=switch_json["payload"]) + message.encode() + return message + + +@pytest.fixture(name="sensor_msg") +async def sensor_msg_fixture(hass): + """Return a mock MQTT msg with a sensor change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/sensor.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message + + +@pytest.fixture(name="binary_sensor_msg") +async def binary_sensor_msg_fixture(hass): + """Return a mock MQTT msg with a binary_sensor change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/binary_sensor.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message + + +@pytest.fixture(name="binary_sensor_alt_msg") +async def binary_sensor_alt_msg_fixture(hass): + """Return a mock MQTT msg with a binary_sensor change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/binary_sensor_alt.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py new file mode 100644 index 00000000000..62b23be0cca --- /dev/null +++ b/tests/components/ozw/test_binary_sensor.py @@ -0,0 +1,66 @@ +"""Test Z-Wave Sensors.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DOMAIN as BINARY_SENSOR_DOMAIN, +) +from homeassistant.components.ozw.const import DOMAIN +from homeassistant.const import ATTR_DEVICE_CLASS + +from .common import setup_ozw + + +async def test_binary_sensor(hass, generic_data, binary_sensor_msg): + """Test setting up config entry.""" + receive_msg = await setup_ozw(hass, fixture=generic_data) + + # Test Legacy sensor (disabled by default) + registry = await hass.helpers.entity_registry.async_get_registry() + entity_id = "binary_sensor.trisensor_sensor" + state = hass.states.get(entity_id) + assert state is None + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + + # Test enabling legacy entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + # Test Sensor for Notification CC + state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") + assert state + assert state.state == "off" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MOTION + + # Test incoming state change + receive_msg(binary_sensor_msg) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") + assert state.state == "on" + + +async def test_sensor_enabled(hass, generic_data, binary_sensor_alt_msg): + """Test enabling a legacy binary_sensor.""" + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + "1-37-625737744", + suggested_object_id="trisensor_sensor_instance_1_sensor", + disabled_by=None, + ) + assert entry.disabled is False + + receive_msg = await setup_ozw(hass, fixture=generic_data) + receive_msg(binary_sensor_alt_msg) + await hass.async_block_till_done() + + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "on" diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py new file mode 100644 index 00000000000..bfe3f922402 --- /dev/null +++ b/tests/components/ozw/test_config_flow.py @@ -0,0 +1,53 @@ +"""Test the Z-Wave over MQTT config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.ozw.config_flow import TITLE +from homeassistant.components.ozw.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_user_create_entry(hass): + """Test the user step creates an entry.""" + hass.config.components.add("mqtt") + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == "create_entry" + assert result2["title"] == TITLE + assert result2["data"] == {} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_mqtt_not_setup(hass): + """Test that mqtt is required.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "mqtt_required" + + +async def test_one_instance_allowed(hass): + """Test that only one instance is allowed.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "one_instance_allowed" diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py new file mode 100644 index 00000000000..9a76b4906fa --- /dev/null +++ b/tests/components/ozw/test_init.py @@ -0,0 +1,62 @@ +"""Test integration initialization.""" +from homeassistant import config_entries +from homeassistant.components.ozw import DOMAIN, PLATFORMS, const + +from .common import setup_ozw + +from tests.common import MockConfigEntry + + +async def test_init_entry(hass, generic_data): + """Test setting up config entry.""" + await setup_ozw(hass, fixture=generic_data) + + # Verify integration + platform loaded. + assert "ozw" in hass.config.components + for platform in PLATFORMS: + assert platform in hass.config.components, platform + assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" + + # Verify services registered + assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) + assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) + + +async def test_unload_entry(hass, generic_data, switch_msg, caplog): + """Test unload the config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + ) + entry.add_to_hass(hass) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + receive_message = await setup_ozw(hass, entry=entry, fixture=generic_data) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(hass.states.async_entity_ids("switch")) == 1 + + await hass.config_entries.async_unload(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.states.async_entity_ids("switch")) == 0 + + # Send a message for a switch from the broker to check that + # all entity topic subscribers are unsubscribed. + receive_message(switch_msg) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("switch")) == 0 + + # Load the integration again and check that there are no errors when + # adding the entities. + # This asserts that we have unsubscribed the entity addition signals + # when unloading the integration previously. + await setup_ozw(hass, entry=entry, fixture=generic_data) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(hass.states.async_entity_ids("switch")) == 1 + for record in caplog.records: + assert record.levelname != "ERROR" diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py new file mode 100644 index 00000000000..d485ca768c5 --- /dev/null +++ b/tests/components/ozw/test_light.py @@ -0,0 +1,151 @@ +"""Test Z-Wave Lights.""" +from homeassistant.components.ozw.light import byte_to_zwave_brightness + +from .common import setup_ozw + + +async def test_light(hass, light_data, light_msg, sent_messages): + """Test setting up config entry.""" + receive_message = await setup_ozw(hass, fixture=light_data) + + # Test loaded + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state is not None + assert state.state == "off" + + # Test turning on + # Beware that due to rounding, a roundtrip conversion does not always work + new_brightness = 44 + new_transition = 0 + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.led_bulb_6_multi_colour_level", + "brightness": new_brightness, + "transition": new_transition, + }, + blocking=True, + ) + assert len(sent_messages) == 2 + + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 0, "ValueIDKey": 1407375551070225} + + msg = sent_messages[1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": byte_to_zwave_brightness(new_brightness), + "ValueIDKey": 659128337, + } + + # Feedback on state + light_msg.decode() + light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) + light_msg.encode() + receive_message(light_msg) + await hass.async_block_till_done() + + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state is not None + assert state.state == "on" + assert state.attributes["brightness"] == new_brightness + + # Test turning off + new_transition = 6553 + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": "light.led_bulb_6_multi_colour_level", + "transition": new_transition, + }, + blocking=True, + ) + assert len(sent_messages) == 4 + + msg = sent_messages[-2] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 237, "ValueIDKey": 1407375551070225} + + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 0, "ValueIDKey": 659128337} + + # Feedback on state + light_msg.decode() + light_msg.payload["Value"] = 0 + light_msg.encode() + receive_message(light_msg) + await hass.async_block_till_done() + + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state is not None + assert state.state == "off" + + # Test turn on without brightness + new_transition = 127 + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.led_bulb_6_multi_colour_level", + "transition": new_transition, + }, + blocking=True, + ) + assert len(sent_messages) == 6 + + msg = sent_messages[-2] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 127, "ValueIDKey": 1407375551070225} + + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": 255, + "ValueIDKey": 659128337, + } + + # Feedback on state + light_msg.decode() + light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) + light_msg.encode() + receive_message(light_msg) + await hass.async_block_till_done() + + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state is not None + assert state.state == "on" + assert state.attributes["brightness"] == new_brightness + + # Test set brightness to 0 + new_brightness = 0 + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.led_bulb_6_multi_colour_level", + "brightness": new_brightness, + }, + blocking=True, + ) + assert len(sent_messages) == 7 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": byte_to_zwave_brightness(new_brightness), + "ValueIDKey": 659128337, + } + + # Feedback on state + light_msg.decode() + light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) + light_msg.encode() + receive_message(light_msg) + await hass.async_block_till_done() + + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state is not None + assert state.state == "off" diff --git a/tests/components/ozw/test_scenes.py b/tests/components/ozw/test_scenes.py new file mode 100644 index 00000000000..2d776b7faf4 --- /dev/null +++ b/tests/components/ozw/test_scenes.py @@ -0,0 +1,88 @@ +"""Test Z-Wave (central) Scenes.""" +from .common import MQTTMessage, setup_ozw + +from tests.common import async_capture_events + + +async def test_scenes(hass, generic_data, sent_messages): + """Test setting up config entry.""" + + receive_message = await setup_ozw(hass, fixture=generic_data) + events = async_capture_events(hass, "ozw.scene_activated") + + # Publish fake scene event on mqtt + message = MQTTMessage( + topic="OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/", + payload={ + "Label": "Scene", + "Value": 16, + "Units": "", + "Min": -2147483648, + "Max": 2147483647, + "Type": "Int", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", + "Index": 0, + "Node": 7, + "Genre": "User", + "Help": "", + "ValueIDKey": 122339347, + "ReadOnly": False, + "WriteOnly": False, + "ValueSet": False, + "ValuePolled": False, + "ChangeVerified": False, + "Event": "valueChanged", + "TimeStamp": 1579630367, + }, + ) + message.encode() + receive_message(message) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["scene_value_id"] == 16 + + # Publish fake central scene event on mqtt + message = MQTTMessage( + topic="OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/", + payload={ + "Label": "Scene 1", + "Value": { + "List": [ + {"Value": 0, "Label": "Inactive"}, + {"Value": 1, "Label": "Pressed 1 Time"}, + {"Value": 2, "Label": "Key Released"}, + {"Value": 3, "Label": "Key Held down"}, + ], + "Selected": "Pressed 1 Time", + "Selected_id": 1, + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", + "Index": 1, + "Node": 61, + "Genre": "User", + "Help": "", + "ValueIDKey": 281476005806100, + "ReadOnly": False, + "WriteOnly": False, + "ValueSet": False, + "ValuePolled": False, + "ChangeVerified": False, + "Event": "valueChanged", + "TimeStamp": 1579640710, + }, + ) + message.encode() + receive_message(message) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 2 + assert events[1].data["scene_id"] == 1 + assert events[1].data["scene_label"] == "Scene 1" + assert events[1].data["scene_value_label"] == "Pressed 1 Time" diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py new file mode 100644 index 00000000000..4cc0077cdea --- /dev/null +++ b/tests/components/ozw/test_sensor.py @@ -0,0 +1,76 @@ +"""Test Z-Wave Sensors.""" +from homeassistant.components.ozw.const import DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.const import ATTR_DEVICE_CLASS + +from .common import setup_ozw + + +async def test_sensor(hass, generic_data): + """Test setting up config entry.""" + await setup_ozw(hass, fixture=generic_data) + + # Test standard sensor + state = hass.states.get("sensor.smart_plug_electric_v") + assert state is not None + assert state.state == "123.9" + assert state.attributes["unit_of_measurement"] == "V" + + # Test device classes + state = hass.states.get("sensor.trisensor_relative_humidity") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_HUMIDITY + state = hass.states.get("sensor.trisensor_pressure") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_PRESSURE + state = hass.states.get("sensor.trisensor_fake_power") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + state = hass.states.get("sensor.trisensor_fake_energy") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + state = hass.states.get("sensor.trisensor_fake_electric") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + + # Test ZWaveListSensor disabled by default + registry = await hass.helpers.entity_registry.async_get_registry() + entity_id = "sensor.water_sensor_6_instance_1_water" + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + + # Test enabling entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def test_sensor_enabled(hass, generic_data, sensor_msg): + """Test enabling an advanced sensor.""" + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "1-36-1407375493578772", + suggested_object_id="water_sensor_6_instance_1_water", + disabled_by=None, + ) + assert entry.disabled is False + + receive_msg = await setup_ozw(hass, fixture=generic_data) + receive_msg(sensor_msg) + await hass.async_block_till_done() + + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "0" + assert state.attributes["label"] == "Clear" diff --git a/tests/components/ozw/test_switch.py b/tests/components/ozw/test_switch.py new file mode 100644 index 00000000000..7af331b3e0f --- /dev/null +++ b/tests/components/ozw/test_switch.py @@ -0,0 +1,41 @@ +"""Test Z-Wave Switches.""" +from .common import setup_ozw + + +async def test_switch(hass, generic_data, sent_messages, switch_msg): + """Test setting up config entry.""" + receive_message = await setup_ozw(hass, fixture=generic_data) + + # Test loaded + state = hass.states.get("switch.smart_plug_switch") + assert state is not None + assert state.state == "off" + + # Test turning on + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.smart_plug_switch"}, blocking=True + ) + assert len(sent_messages) == 1 + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": True, "ValueIDKey": 541671440} + + # Feedback on state + switch_msg.decode() + switch_msg.payload["Value"] = True + switch_msg.encode() + receive_message(switch_msg) + await hass.async_block_till_done() + + state = hass.states.get("switch.smart_plug_switch") + assert state is not None + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.smart_plug_switch"}, blocking=True + ) + assert len(sent_messages) == 2 + msg = sent_messages[1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 541671440} diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index fd2738508ea..0e7731dbdc0 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -1,7 +1,4 @@ """Test the Panasonic Viera config flow.""" -from unittest.mock import Mock - -from asynctest import patch from panasonic_viera import TV_TYPE_ENCRYPTED, TV_TYPE_NONENCRYPTED, SOAPError import pytest @@ -20,6 +17,7 @@ from homeassistant.components.panasonic_viera.const import ( ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py new file mode 100644 index 00000000000..263f2def9af --- /dev/null +++ b/tests/components/panasonic_viera/test_init.py @@ -0,0 +1,122 @@ +"""Test the Panasonic Viera setup process.""" +from asynctest import patch + +from homeassistant.components.panasonic_viera.const import ( + CONF_APP_ID, + CONF_ENCRYPTION_KEY, + CONF_ON_ACTION, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.async_mock import Mock +from tests.common import MockConfigEntry + +MOCK_CONFIG_DATA = { + CONF_HOST: "0.0.0.0", + CONF_NAME: DEFAULT_NAME, + CONF_PORT: DEFAULT_PORT, + CONF_ON_ACTION: None, +} + +MOCK_ENCRYPTION_DATA = { + CONF_APP_ID: "mock-app-id", + CONF_ENCRYPTION_KEY: "mock-encryption-key", +} + + +def get_mock_remote(): + """Return a mock remote.""" + mock_remote = Mock() + + async def async_create_remote_control(during_setup=False): + return + + mock_remote.async_create_remote_control = async_create_remote_control + + return mock_remote + + +async def test_setup_entry_encrypted(hass): + """Test setup with encrypted config entry.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_CONFIG_DATA[CONF_HOST], + data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA}, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_entry_unencrypted(hass): + """Test setup with unencrypted config entry.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_config_flow_initiated(hass): + """Test if config flow is initiated in setup.""" + assert ( + await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_HOST: "0.0.0.0"}},) + is True + ) + + assert len(hass.config_entries.flow.async_progress()) == 1 + + +async def test_setup_unload_entry(hass): + """Test if config entry is unloaded.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == ENTRY_STATE_NOT_LOADED + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state is None diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index 5f7161089f6..c2abd673065 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -1,9 +1,9 @@ """The tests for the panel_custom component.""" -from unittest.mock import Mock, patch - from homeassistant import setup from homeassistant.components import frontend +from tests.async_mock import Mock, patch + async def test_webcomponent_custom_path_not_found(hass): """Test if a web component is found in config panels dir.""" diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 76350619983..887f0d94fef 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,7 +1,6 @@ """The tests for the person component.""" import logging -from asynctest import patch import pytest from homeassistant.components import person @@ -24,6 +23,7 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import collection, entity_registry from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, mock_component, mock_restore_cache DEVICE_TRACKER = "device_tracker.test_tracker" diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 7eea15b79c8..b39bfdced2a 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -1 +1,71 @@ """Tests for the pi_hole component.""" +from hole.exceptions import HoleError + +from homeassistant.components.pi_hole.const import CONF_LOCATION +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) + +from tests.async_mock import AsyncMock, MagicMock, patch + +ZERO_DATA = { + "ads_blocked_today": 0, + "ads_percentage_today": 0, + "clients_ever_seen": 0, + "dns_queries_today": 0, + "domains_being_blocked": 0, + "queries_cached": 0, + "queries_forwarded": 0, + "status": 0, + "unique_clients": 0, + "unique_domains": 0, +} + +HOST = "1.2.3.4" +PORT = 80 +LOCATION = "location" +NAME = "name" +API_KEY = "apikey" +SSL = False +VERIFY_SSL = True + +CONF_DATA = { + CONF_HOST: f"{HOST}:{PORT}", + CONF_LOCATION: LOCATION, + CONF_NAME: NAME, + CONF_API_KEY: API_KEY, + CONF_SSL: SSL, + CONF_VERIFY_SSL: VERIFY_SSL, +} + +CONF_CONFIG_FLOW = { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_LOCATION: LOCATION, + CONF_NAME: NAME, + CONF_API_KEY: API_KEY, + CONF_SSL: SSL, + CONF_VERIFY_SSL: VERIFY_SSL, +} + + +def _create_mocked_hole(raise_exception=False): + mocked_hole = MagicMock() + type(mocked_hole).get_data = AsyncMock( + side_effect=HoleError("") if raise_exception else None + ) + type(mocked_hole).enable = AsyncMock() + type(mocked_hole).disable = AsyncMock() + mocked_hole.data = ZERO_DATA + return mocked_hole + + +def _patch_config_flow_hole(mocked_hole): + return patch( + "homeassistant.components.pi_hole.config_flow.Hole", return_value=mocked_hole + ) diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py new file mode 100644 index 00000000000..32b5b1ca146 --- /dev/null +++ b/tests/components/pi_hole/test_config_flow.py @@ -0,0 +1,125 @@ +"""Test pi_hole config flow.""" +import copy +import logging + +from homeassistant.components.pi_hole.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import ( + CONF_CONFIG_FLOW, + CONF_DATA, + CONF_HOST, + NAME, + _create_mocked_hole, + _patch_config_flow_hole, +) + +from tests.async_mock import patch + + +def _flow_next(hass, flow_id): + return next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == flow_id + ) + + +def _patch_setup(): + return patch( + "homeassistant.components.pi_hole.async_setup_entry", return_value=True, + ) + + +async def test_flow_import(hass, caplog): + """Test import flow.""" + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + # duplicated server + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # duplicated name + conf_data = copy.deepcopy(CONF_DATA) + conf_data[CONF_HOST] = "4.3.2.1" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf_data + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "duplicated_name" + assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 + + +async def test_flow_import_invalid(hass, caplog): + """Test import flow with invalid server.""" + mocked_hole = _create_mocked_hole(True) + with _patch_config_flow_hole(mocked_hole), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 + + +async def test_flow_user(hass): + """Test user initialized flow.""" + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + _flow_next(hass, result["flow_id"]) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONF_CONFIG_FLOW, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + # duplicated server + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # duplicated name + conf_data = copy.deepcopy(CONF_CONFIG_FLOW) + conf_data[CONF_HOST] = "4.3.2.1" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf_data + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "duplicated_name" + + +async def test_flow_user_invalid(hass): + """Test user initialized flow with invalid server.""" + mocked_hole = _create_mocked_hole(True) + with _patch_config_flow_hole(mocked_hole), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index c2d9ec77f03..73a501c74ce 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -1,33 +1,21 @@ """Test pi_hole component.""" -from unittest.mock import patch - -from asynctest import CoroutineMock - from homeassistant.components import pi_hole +from . import _create_mocked_hole, _patch_config_flow_hole + +from tests.async_mock import patch from tests.common import async_setup_component -ZERO_DATA = { - "ads_blocked_today": 0, - "ads_percentage_today": 0, - "clients_ever_seen": 0, - "dns_queries_today": 0, - "domains_being_blocked": 0, - "queries_cached": 0, - "queries_forwarded": 0, - "status": 0, - "unique_clients": 0, - "unique_domains": 0, -} + +def _patch_init_hole(mocked_hole): + return patch("homeassistant.components.pi_hole.Hole", return_value=mocked_hole) async def test_setup_minimal_config(hass): """Tests component setup with minimal config.""" - with patch("homeassistant.components.pi_hole.Hole") as _hole: - _hole.return_value.get_data = CoroutineMock(return_value=None) - _hole.return_value.data = ZERO_DATA - + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): assert await async_setup_component( hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} ) @@ -81,10 +69,8 @@ async def test_setup_minimal_config(hass): async def test_setup_name_config(hass): """Tests component setup with a custom name.""" - with patch("homeassistant.components.pi_hole.Hole") as _hole: - _hole.return_value.get_data = CoroutineMock(return_value=None) - _hole.return_value.data = ZERO_DATA - + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): assert await async_setup_component( hass, pi_hole.DOMAIN, @@ -101,19 +87,15 @@ async def test_setup_name_config(hass): async def test_disable_service_call(hass): """Test disable service call with no Pi-hole named.""" - with patch("homeassistant.components.pi_hole.Hole") as _hole: - mock_disable = CoroutineMock(return_value=None) - _hole.return_value.disable = mock_disable - _hole.return_value.get_data = CoroutineMock(return_value=None) - _hole.return_value.data = ZERO_DATA - + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): assert await async_setup_component( hass, pi_hole.DOMAIN, { pi_hole.DOMAIN: [ - {"host": "pi.hole", "api_key": "1"}, - {"host": "pi.hole", "name": "Custom", "api_key": "2"}, + {"host": "pi.hole1", "api_key": "1"}, + {"host": "pi.hole2", "name": "Custom", "api_key": "2"}, ] }, ) @@ -129,24 +111,20 @@ async def test_disable_service_call(hass): await hass.async_block_till_done() - assert mock_disable.call_count == 2 + assert mocked_hole.disable.call_count == 2 async def test_enable_service_call(hass): """Test enable service call with no Pi-hole named.""" - with patch("homeassistant.components.pi_hole.Hole") as _hole: - mock_enable = CoroutineMock(return_value=None) - _hole.return_value.enable = mock_enable - _hole.return_value.get_data = CoroutineMock(return_value=None) - _hole.return_value.data = ZERO_DATA - + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): assert await async_setup_component( hass, pi_hole.DOMAIN, { pi_hole.DOMAIN: [ - {"host": "pi.hole", "api_key": "1"}, - {"host": "pi.hole", "name": "Custom", "api_key": "2"}, + {"host": "pi.hole1", "api_key": "1"}, + {"host": "pi.hole2", "name": "Custom", "api_key": "2"}, ] }, ) @@ -159,4 +137,4 @@ async def test_enable_service_call(hass): await hass.async_block_till_done() - assert mock_enable.call_count == 2 + assert mocked_hole.enable.call_count == 2 diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index d9b4f4859bb..53b1ec3a94d 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging import socket import unittest -from unittest.mock import patch import pytest @@ -12,6 +11,7 @@ from homeassistant.components import pilight from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant _LOGGER = logging.getLogger(__name__) @@ -109,7 +109,7 @@ class TestPilight(unittest.TestCase): @patch("pilight.pilight.Client", PilightDaemonSim) @patch("homeassistant.core._LOGGER.error") - @patch("tests.components.test_pilight._LOGGER.error") + @patch("homeassistant.components.pilight._LOGGER.error") def test_send_code_no_protocol(self, mock_pilight_error, mock_error): """Try to send data without protocol information, should give error.""" with assert_setup_component(4): @@ -127,7 +127,7 @@ class TestPilight(unittest.TestCase): assert "required key not provided @ data['protocol']" in str(error_log_call) @patch("pilight.pilight.Client", PilightDaemonSim) - @patch("tests.components.test_pilight._LOGGER.error") + @patch("homeassistant.components.pilight._LOGGER.error") def test_send_code(self, mock_pilight_error): """Try to send proper data.""" with assert_setup_component(4): @@ -167,7 +167,7 @@ class TestPilight(unittest.TestCase): assert "Pilight send failed" in str(error_log_call) @patch("pilight.pilight.Client", PilightDaemonSim) - @patch("tests.components.test_pilight._LOGGER.error") + @patch("homeassistant.components.pilight._LOGGER.error") def test_send_code_delay(self, mock_pilight_error): """Try to send proper data with delay afterwards.""" with assert_setup_component(4): @@ -207,7 +207,7 @@ class TestPilight(unittest.TestCase): assert str(service_data2) in str(error_log_call) @patch("pilight.pilight.Client", PilightDaemonSim) - @patch("tests.components.test_pilight._LOGGER.error") + @patch("homeassistant.components.pilight._LOGGER.error") def test_start_stop(self, mock_pilight_error): """Check correct startup and stop of pilight daemon.""" with assert_setup_component(4): diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 801061e94ea..f7260a5cf2e 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -1,7 +1,5 @@ """Unit tests for platform/plant.py.""" -import asyncio from datetime import datetime, timedelta -import unittest import pytest @@ -15,9 +13,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.setup import setup_component +from homeassistant.core import State +from homeassistant.setup import async_setup_component -from tests.common import get_test_home_assistant, init_recorder_component +from tests.common import init_recorder_component GOOD_DATA = { "moisture": 50, @@ -47,206 +46,193 @@ GOOD_CONFIG = { } -class _MockState: - def __init__(self, state=None): - self.state = state - - -class TestPlant(unittest.TestCase): - """Tests for component "plant".""" - - def setUp(self): - """Create test instance of Home Assistant.""" - self.hass = get_test_home_assistant() - self.hass.start() - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @asyncio.coroutine - def test_valid_data(self): - """Test processing valid data.""" - sensor = plant.Plant("my plant", GOOD_CONFIG) - sensor.hass = self.hass - for reading, value in GOOD_DATA.items(): - sensor.state_changed( - GOOD_CONFIG["sensors"][reading], None, _MockState(value) - ) - assert sensor.state == "ok" - attrib = sensor.state_attributes - for reading, value in GOOD_DATA.items(): - # battery level has a different name in - # the JSON format than in hass - assert attrib[reading] == value - - @asyncio.coroutine - def test_low_battery(self): - """Test processing with low battery data and limit set.""" - sensor = plant.Plant("other plant", GOOD_CONFIG) - sensor.hass = self.hass - assert sensor.state_attributes["problem"] == "none" +async def test_valid_data(hass): + """Test processing valid data.""" + sensor = plant.Plant("my plant", GOOD_CONFIG) + sensor.entity_id = "sensor.mqtt_plant_battery" + sensor.hass = hass + for reading, value in GOOD_DATA.items(): sensor.state_changed( - "sensor.mqtt_plant_battery", _MockState(45), _MockState(10) + GOOD_CONFIG["sensors"][reading], + None, + State(GOOD_CONFIG["sensors"][reading], value), ) - assert sensor.state == "problem" - assert sensor.state_attributes["problem"] == "battery low" + assert sensor.state == "ok" + attrib = sensor.state_attributes + for reading, value in GOOD_DATA.items(): + # battery level has a different name in + # the JSON format than in hass + assert attrib[reading] == value - def test_initial_states(self): - """Test plant initialises attributes if sensor already exists.""" - self.hass.states.set( - MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} - ) - plant_name = "some_plant" - assert setup_component( - self.hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} - ) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert 5 == state.attributes[plant.READING_MOISTURE] - def test_update_states(self): - """Test updating the state of a sensor. - - Make sure that plant processes this correctly. - """ - plant_name = "some_plant" - assert setup_component( - self.hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} - ) - self.hass.states.set( - MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} - ) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert STATE_PROBLEM == state.state - assert 5 == state.attributes[plant.READING_MOISTURE] - - def test_unavailable_state(self): - """Test updating the state with unavailable. - - Make sure that plant processes this correctly. - """ - plant_name = "some_plant" - assert setup_component( - self.hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} - ) - self.hass.states.set( - MOISTURE_ENTITY, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} - ) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert state.state == STATE_PROBLEM - assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE - - def test_state_problem_if_unavailable(self): - """Test updating the state with unavailable after setting it to valid value. - - Make sure that plant processes this correctly. - """ - plant_name = "some_plant" - assert setup_component( - self.hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} - ) - self.hass.states.set( - MOISTURE_ENTITY, 42, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} - ) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert state.state == STATE_OK - assert state.attributes[plant.READING_MOISTURE] == 42 - self.hass.states.set( - MOISTURE_ENTITY, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} - ) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert state.state == STATE_PROBLEM - assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE - - @pytest.mark.skipif( - plant.ENABLE_LOAD_HISTORY is False, - reason="tests for loading from DB are unstable, thus" - "this feature is turned of until tests become" - "stable", +async def test_low_battery(hass): + """Test processing with low battery data and limit set.""" + sensor = plant.Plant("other plant", GOOD_CONFIG) + sensor.entity_id = "sensor.mqtt_plant_battery" + sensor.hass = hass + assert sensor.state_attributes["problem"] == "none" + sensor.state_changed( + "sensor.mqtt_plant_battery", + State("sensor.mqtt_plant_battery", 45), + State("sensor.mqtt_plant_battery", 10), ) - def test_load_from_db(self): - """Test bootstrapping the brightness history from the database. + assert sensor.state == "problem" + assert sensor.state_attributes["problem"] == "battery low" - This test can should only be executed if the loading of the history - is enabled via plant.ENABLE_LOAD_HISTORY. - """ - init_recorder_component(self.hass) - plant_name = "wise_plant" - for value in [20, 30, 10]: - self.hass.states.set( - BRIGHTNESS_ENTITY, value, {ATTR_UNIT_OF_MEASUREMENT: "Lux"} - ) - self.hass.block_till_done() - # wait for the recorder to really store the data - self.hass.data[recorder.DATA_INSTANCE].block_till_done() +async def test_initial_states(hass): + """Test plant initialises attributes if sensor already exists.""" + hass.states.async_set(MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY}) + plant_name = "some_plant" + assert await async_setup_component( + hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + ) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert 5 == state.attributes[plant.READING_MOISTURE] - assert setup_component( - self.hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + +async def test_update_states(hass): + """Test updating the state of a sensor. + + Make sure that plant processes this correctly. + """ + plant_name = "some_plant" + assert await async_setup_component( + hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + ) + hass.states.async_set(MOISTURE_ENTITY, 5, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY}) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert STATE_PROBLEM == state.state + assert 5 == state.attributes[plant.READING_MOISTURE] + + +async def test_unavailable_state(hass): + """Test updating the state with unavailable. + + Make sure that plant processes this correctly. + """ + plant_name = "some_plant" + assert await async_setup_component( + hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + ) + hass.states.async_set( + MOISTURE_ENTITY, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} + ) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert state.state == STATE_PROBLEM + assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE + + +async def test_state_problem_if_unavailable(hass): + """Test updating the state with unavailable after setting it to valid value. + + Make sure that plant processes this correctly. + """ + plant_name = "some_plant" + assert await async_setup_component( + hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + ) + hass.states.async_set(MOISTURE_ENTITY, 42, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY}) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert state.state == STATE_OK + assert state.attributes[plant.READING_MOISTURE] == 42 + hass.states.async_set( + MOISTURE_ENTITY, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: CONDUCTIVITY} + ) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert state.state == STATE_PROBLEM + assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE + + +@pytest.mark.skipif( + plant.ENABLE_LOAD_HISTORY is False, + reason="tests for loading from DB are unstable, thus" + "this feature is turned of until tests become" + "stable", +) +async def test_load_from_db(hass): + """Test bootstrapping the brightness history from the database. + + This test can should only be executed if the loading of the history + is enabled via plant.ENABLE_LOAD_HISTORY. + """ + init_recorder_component(hass) + plant_name = "wise_plant" + for value in [20, 30, 10]: + + hass.states.async_set( + BRIGHTNESS_ENTITY, value, {ATTR_UNIT_OF_MEASUREMENT: "Lux"} ) - self.hass.block_till_done() + await hass.async_block_till_done() + # wait for the recorder to really store the data + hass.data[recorder.DATA_INSTANCE].block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert STATE_UNKNOWN == state.state - max_brightness = state.attributes.get(plant.ATTR_MAX_BRIGHTNESS_HISTORY) - assert 30 == max_brightness + assert await async_setup_component( + hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + ) + await hass.async_block_till_done() - def test_brightness_history(self): - """Test the min_brightness check.""" - plant_name = "some_plant" - assert setup_component( - self.hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} - ) - self.hass.states.set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert STATE_PROBLEM == state.state - - self.hass.states.set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert STATE_OK == state.state - - self.hass.states.set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) - self.hass.block_till_done() - state = self.hass.states.get(f"plant.{plant_name}") - assert STATE_OK == state.state + state = hass.states.get(f"plant.{plant_name}") + assert STATE_UNKNOWN == state.state + max_brightness = state.attributes.get(plant.ATTR_MAX_BRIGHTNESS_HISTORY) + assert 30 == max_brightness -class TestDailyHistory(unittest.TestCase): - """Test the DailyHistory helper class.""" +async def test_brightness_history(hass): + """Test the min_brightness check.""" + plant_name = "some_plant" + assert await async_setup_component( + hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} + ) + hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert STATE_PROBLEM == state.state - def test_no_data(self): - """Test with empty history.""" - dh = plant.DailyHistory(3) - assert dh.max is None + hass.states.async_set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert STATE_OK == state.state - def test_one_day(self): - """Test storing data for the same day.""" - dh = plant.DailyHistory(3) - values = [-2, 10, 0, 5, 20] - for i in range(len(values)): - dh.add_measurement(values[i]) - max_value = max(values[0 : i + 1]) - assert 1 == len(dh._days) - assert dh.max == max_value + hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) + await hass.async_block_till_done() + state = hass.states.get(f"plant.{plant_name}") + assert STATE_OK == state.state - def test_multiple_days(self): - """Test storing data for different days.""" - dh = plant.DailyHistory(3) - today = datetime.now() - today_minus_1 = today - timedelta(days=1) - today_minus_2 = today_minus_1 - timedelta(days=1) - today_minus_3 = today_minus_2 - timedelta(days=1) - days = [today_minus_3, today_minus_2, today_minus_1, today] - values = [10, 1, 7, 3] - max_values = [10, 10, 10, 7] - for i in range(len(days)): - dh.add_measurement(values[i], days[i]) - assert max_values[i] == dh.max +def test_daily_history_no_data(hass): + """Test with empty history.""" + dh = plant.DailyHistory(3) + assert dh.max is None + + +def test_daily_history_one_day(hass): + """Test storing data for the same day.""" + dh = plant.DailyHistory(3) + values = [-2, 10, 0, 5, 20] + for i in range(len(values)): + dh.add_measurement(values[i]) + max_value = max(values[0 : i + 1]) + assert 1 == len(dh._days) + assert dh.max == max_value + + +def test_daily_history_multiple_days(hass): + """Test storing data for different days.""" + dh = plant.DailyHistory(3) + today = datetime.now() + today_minus_1 = today - timedelta(days=1) + today_minus_2 = today_minus_1 - timedelta(days=1) + today_minus_3 = today_minus_2 - timedelta(days=1) + days = [today_minus_3, today_minus_2, today_minus_1, today] + values = [10, 1, 7, 3] + max_values = [10, 10, 10, 7] + + for i in range(len(days)): + dh.add_measurement(values[i], days[i]) + assert max_values[i] == dh.max diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 9b59190173f..ec1b490ddf5 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -8,6 +8,32 @@ from homeassistant.const import CONF_URL from .const import DEFAULT_DATA, MOCK_SERVERS, MOCK_USERS +GDM_PAYLOAD = [ + { + "data": { + "Content-Type": "plex/media-server", + "Name": "plextest", + "Port": "32400", + "Resource-Identifier": "1234567890123456789012345678901234567890", + "Updated-At": "157762684800", + "Version": "1.0", + }, + "from": ("1.2.3.4", 32414), + } +] + + +class MockGDM: + """Mock a GDM instance.""" + + def __init__(self): + """Initialize the object.""" + self.entries = GDM_PAYLOAD + + def scan(self): + """Mock the scan call.""" + pass + class MockResource: """Mock a PlexAccount resource.""" @@ -134,6 +160,7 @@ class MockPlexClient: """Initialize the object.""" self.machineIdentifier = f"client-{index+1}" self._baseurl = url + self._index = index def url(self, key): """Mock the url method.""" @@ -152,6 +179,8 @@ class MockPlexClient: @property def product(self): """Mock the product attribute.""" + if self._index == 1: + return "Plex Web" return "PRODUCT" @property diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index d839ccc674b..b43448b4d97 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,49 +1,68 @@ """Tests for Plex config flow.""" import copy +import ssl -from asynctest import patch import plexapi.exceptions import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex import config_flow from homeassistant.components.plex.const import ( + AUTOMATIC_SETUP_STRING, CONF_IGNORE_NEW_SHARED_USERS, + CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, DOMAIN, + MANUAL_SETUP_STRING, PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) -from homeassistant.config_entries import ENTRY_STATE_LOADED -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + SOURCE_INTEGRATION_DISCOVERY, +) +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.setup import async_setup_component from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN -from .mock_classes import MockPlexAccount, MockPlexServer +from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer +from tests.async_mock import patch from tests.common import MockConfigEntry async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -52,8 +71,8 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - assert result["errors"]["base"] == "faulty_credentials" + assert result["step_id"] == "user" + assert result["errors"][CONF_TOKEN] == "faulty_credentials" async def test_import_success(hass): @@ -99,17 +118,22 @@ async def test_import_bad_hostname(hass): async def test_unknown_exception(hass): """Test when an unknown exception is encountered.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), patch( "plexauth.PlexAuth.initiate_auth" ), patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -123,20 +147,24 @@ async def test_unknown_exception(hass): async def test_no_servers_found(hass): """Test when no servers are on an account.""" - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -144,7 +172,7 @@ async def test_no_servers_found(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" assert result["errors"]["base"] == "no_servers" @@ -153,20 +181,24 @@ async def test_single_available_server(hass): mock_plex_server = MockPlexServer() - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( "plexapi.server.PlexServer", return_value=mock_plex_server ), patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -188,13 +220,15 @@ async def test_multiple_servers_with_selection(hass): mock_plex_server = MockPlexServer() - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) @@ -203,7 +237,9 @@ async def test_multiple_servers_with_selection(hass): ), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -231,7 +267,9 @@ async def test_adding_last_unconfigured_server(hass): mock_plex_server = MockPlexServer() - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) MockConfigEntry( domain=DOMAIN, @@ -245,7 +283,7 @@ async def test_adding_last_unconfigured_server(hass): DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) @@ -254,7 +292,9 @@ async def test_adding_last_unconfigured_server(hass): ), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -303,7 +343,9 @@ async def test_already_configured(hass): async def test_all_available_servers_configured(hass): """Test when all available servers are already configured.""" - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) MockConfigEntry( domain=DOMAIN, @@ -325,14 +367,16 @@ async def test_all_available_servers_configured(hass): DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -388,6 +432,7 @@ async def test_option_flow(hass): CONF_MONITORED_USERS: { user: {"enabled": True} for user in mock_plex_server.accounts }, + CONF_IGNORE_PLEX_WEB_CLIENTS: False, } } @@ -443,18 +488,22 @@ async def test_option_flow_new_users_available(hass, caplog): async def test_external_timed_out(hass): """Test when external flow times out.""" - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=None ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -468,18 +517,22 @@ async def test_external_timed_out(hass): async def test_callback_view(hass, aiohttp_client): """Test callback view.""" - await async_setup_component(hass, "http", {"http": {}}) + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" + assert result["step_id"] == "user" with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "external" client = await aiohttp_client(hass.http.app) @@ -502,3 +555,219 @@ async def test_multiple_servers_with_import(hass): ) assert result["type"] == "abort" assert result["reason"] == "non-interactive" + + +async def test_manual_config(hass): + """Test creating via manual configuration.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) + + class WrongCertValidaitionException(requests.exceptions.SSLError): + """Mock the exception showing an unmatched error.""" + + def __init__(self): + self.__context__ = ssl.SSLCertVerificationError( + "some random message that doesn't match" + ) + + # Basic mode + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["data_schema"] is None + hass.config_entries.flow.async_abort(result["flow_id"]) + + # Advanced automatic + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user", "show_advanced_options": True} + ) + + assert result["data_schema"] is not None + assert result["type"] == "form" + assert result["step_id"] == "user_advanced" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"setup_method": AUTOMATIC_SETUP_STRING} + ) + + assert result["type"] == "external" + hass.config_entries.flow.async_abort(result["flow_id"]) + + # Advanced manual + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user", "show_advanced_options": True} + ) + + assert result["data_schema"] is not None + assert result["type"] == "form" + assert result["step_id"] == "user_advanced" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"setup_method": MANUAL_SETUP_STRING} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + mock_plex_server = MockPlexServer() + + MANUAL_SERVER = { + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + CONF_SSL: False, + CONF_VERIFY_SSL: True, + CONF_TOKEN: MOCK_TOKEN, + } + + MANUAL_SERVER_NO_HOST_OR_TOKEN = { + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + CONF_SSL: False, + CONF_VERIFY_SSL: True, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MANUAL_SERVER_NO_HOST_OR_TOKEN + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + assert result["errors"]["base"] == "host_or_token" + + with patch( + "plexapi.server.PlexServer", side_effect=requests.exceptions.SSLError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MANUAL_SERVER + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + assert result["errors"]["base"] == "ssl_error" + + with patch( + "plexapi.server.PlexServer", side_effect=WrongCertValidaitionException, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MANUAL_SERVER + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + assert result["errors"]["base"] == "ssl_error" + + with patch( + "homeassistant.components.plex.PlexServer.connect", + side_effect=requests.exceptions.SSLError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MANUAL_SERVER + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + assert result["errors"]["base"] == "ssl_error" + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MANUAL_SERVER + ) + + assert result["type"] == "create_entry" + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_manual_config_with_token(hass): + """Test creating via manual configuration with only token.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user", "show_advanced_options": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user_advanced" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"setup_method": MANUAL_SETUP_STRING} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + mock_plex_server = MockPlexServer() + + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), patch("homeassistant.components.plex.PlexWebsocket.listen"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "create_entry" + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_setup_with_limited_credentials(hass): + """Test setup with a user with limited permissions.""" + mock_plex_server = MockPlexServer() + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + with patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), patch.object( + mock_plex_server, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized + ) as mock_accounts, patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ) as mock_listen: + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_listen.called + assert mock_accounts.called + + plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machineIdentifier] + assert len(plex_server.accounts) == 0 + assert plex_server.owner is None + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + +async def test_integration_discovery(hass): + """Test integration self-discovery.""" + mock_gdm = MockGDM() + + with patch("homeassistant.components.plex.config_flow.GDM", return_value=mock_gdm): + await config_flow.async_discover(hass) + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + + flow = flows[0] + + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == SOURCE_INTEGRATION_DISCOVERY + assert ( + flow["context"]["unique_id"] + == mock_gdm.entries[0]["data"]["Resource-Identifier"] + ) + assert flow["step_id"] == "user" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index ef2199b11c5..e34476f1813 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -3,9 +3,7 @@ import copy from datetime import timedelta import ssl -from asynctest import ClockedTestCase, patch import plexapi -import pytest import requests from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -31,12 +29,8 @@ import homeassistant.util.dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - async_test_home_assistant, - mock_storage, -) +from tests.async_mock import patch +from tests.common import MockConfigEntry, async_fire_time_changed async def test_setup_with_config(hass): @@ -74,89 +68,86 @@ async def test_setup_with_config(hass): assert loaded_server.plex_server == mock_plex_server -class TestClockedPlex(ClockedTestCase): - """Create clock-controlled asynctest class.""" +# class TestClockedPlex(ClockedTestCase): +# """Create clock-controlled tests.async_mock class.""" - @pytest.fixture(autouse=True) - def inject_fixture(self, caplog): - """Inject pytest fixtures as instance attributes.""" - self.caplog = caplog +# @pytest.fixture(autouse=True) +# def inject_fixture(self, caplog, hass_storage): +# """Inject pytest fixtures as instance attributes.""" +# self.caplog = caplog - async def setUp(self): - """Initialize this test class.""" - self.hass = await async_test_home_assistant(self.loop) - self.mock_storage = mock_storage() - self.mock_storage.__enter__() +# async def setUp(self): +# """Initialize this test class.""" +# self.hass = await async_test_home_assistant(self.loop) - async def tearDown(self): - """Clean up the HomeAssistant instance.""" - await self.hass.async_stop() - self.mock_storage.__exit__(None, None, None) +# async def tearDown(self): +# """Clean up the HomeAssistant instance.""" +# await self.hass.async_stop() - async def test_setup_with_config_entry(self): - """Test setup component with config.""" - hass = self.hass +# async def test_setup_with_config_entry(self): +# """Test setup component with config.""" +# hass = self.hass - mock_plex_server = MockPlexServer() +# mock_plex_server = MockPlexServer() - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) +# entry = MockConfigEntry( +# domain=const.DOMAIN, +# data=DEFAULT_DATA, +# options=DEFAULT_OPTIONS, +# unique_id=DEFAULT_DATA["server_id"], +# ) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() +# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( +# "homeassistant.components.plex.PlexWebsocket.listen" +# ) as mock_listen: +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() - assert mock_listen.called +# assert mock_listen.called - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED +# assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 +# assert entry.state == ENTRY_STATE_LOADED - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] +# server_id = mock_plex_server.machineIdentifier +# loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - assert loaded_server.plex_server == mock_plex_server +# assert loaded_server.plex_server == mock_plex_server - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() +# async_dispatcher_send( +# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) +# ) +# await hass.async_block_till_done() - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) +# sensor = hass.states.get("sensor.plex_plex_server_1") +# assert sensor.state == str(len(mock_plex_server.accounts)) - # Ensure existing entities refresh - await self.advance(const.DEBOUNCE_TIMEOUT) - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() +# # Ensure existing entities refresh +# await self.advance(const.DEBOUNCE_TIMEOUT) +# async_dispatcher_send( +# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) +# ) +# await hass.async_block_till_done() - for test_exception in ( - plexapi.exceptions.BadRequest, - requests.exceptions.RequestException, - ): - with patch.object( - mock_plex_server, "clients", side_effect=test_exception - ) as patched_clients_bad_request: - await self.advance(const.DEBOUNCE_TIMEOUT) - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() +# for test_exception in ( +# plexapi.exceptions.BadRequest, +# requests.exceptions.RequestException, +# ): +# with patch.object( +# mock_plex_server, "clients", side_effect=test_exception +# ) as patched_clients_bad_request: +# await self.advance(const.DEBOUNCE_TIMEOUT) +# async_dispatcher_send( +# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) +# ) +# await hass.async_block_till_done() - assert patched_clients_bad_request.called - assert ( - f"Could not connect to Plex server: {mock_plex_server.friendlyName}" - in self.caplog.text - ) - self.caplog.clear() +# assert patched_clients_bad_request.called +# assert ( +# f"Could not connect to Plex server: {mock_plex_server.friendlyName}" +# in self.caplog.text +# ) +# self.caplog.clear() async def test_set_config_entry_unique_id(hass): diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 6eff97ae7dc..694fcc4885e 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,13 +1,11 @@ """Tests for Plex server.""" import copy -from asynctest import ClockedTestCase, patch - from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, + CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, - DEBOUNCE_TIMEOUT, DOMAIN, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, @@ -17,7 +15,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry, async_test_home_assistant, mock_storage +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_new_users_available(hass): @@ -93,115 +92,155 @@ async def test_new_ignored_users_available(hass, caplog): assert len(monitored_users) == 1 assert len(ignored_users) == 2 for ignored_user in ignored_users: - assert f"Ignoring Plex client owned by '{ignored_user}'" in caplog.text + ignored_client = [ + x.players[0] + for x in mock_plex_server.sessions() + if x.usernames[0] == ignored_user + ][0] + assert ( + f"Ignoring {ignored_client.product} client owned by '{ignored_user}'" + in caplog.text + ) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) -class TestClockedPlex(ClockedTestCase): - """Create clock-controlled asynctest class.""" +# class TestClockedPlex(ClockedTestCase): +# """Create clock-controlled tests.async_mock class.""" - async def setUp(self): - """Initialize this test class.""" - self.hass = await async_test_home_assistant(self.loop) - self.mock_storage = mock_storage() - self.mock_storage.__enter__() +# async def setUp(self): +# """Initialize this test class.""" +# self.hass = await async_test_home_assistant(self.loop) - async def tearDown(self): - """Clean up the HomeAssistant instance.""" - await self.hass.async_stop() - self.mock_storage.__exit__(None, None, None) +# async def tearDown(self): +# """Clean up the HomeAssistant instance.""" +# await self.hass.async_stop() - async def test_mark_sessions_idle(self): - """Test marking media_players as idle when sessions end.""" - hass = self.hass +# async def test_mark_sessions_idle(self): +# """Test marking media_players as idle when sessions end.""" +# hass = self.hass - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) +# entry = MockConfigEntry( +# domain=DOMAIN, +# data=DEFAULT_DATA, +# options=DEFAULT_OPTIONS, +# unique_id=DEFAULT_DATA["server_id"], +# ) - mock_plex_server = MockPlexServer(config_entry=entry) +# mock_plex_server = MockPlexServer(config_entry=entry) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() +# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( +# "homeassistant.components.plex.PlexWebsocket.listen" +# ): +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier +# server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() + +# sensor = hass.states.get("sensor.plex_plex_server_1") +# assert sensor.state == str(len(mock_plex_server.accounts)) + +# mock_plex_server.clear_clients() +# mock_plex_server.clear_sessions() + +# await self.advance(DEBOUNCE_TIMEOUT) +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() + +# sensor = hass.states.get("sensor.plex_plex_server_1") +# assert sensor.state == "0" + +# async def test_debouncer(self): +# """Test debouncer behavior.""" +# hass = self.hass + +# entry = MockConfigEntry( +# domain=DOMAIN, +# data=DEFAULT_DATA, +# options=DEFAULT_OPTIONS, +# unique_id=DEFAULT_DATA["server_id"], +# ) + +# mock_plex_server = MockPlexServer(config_entry=entry) + +# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( +# "homeassistant.components.plex.PlexWebsocket.listen" +# ): +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() + +# server_id = mock_plex_server.machineIdentifier + +# with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( +# mock_plex_server, "sessions", return_value=[] +# ) as mock_update: +# # Called immediately +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 1 + +# # Throttled +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 1 + +# # Throttled +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 1 + +# # Called from scheduler +# await self.advance(DEBOUNCE_TIMEOUT) +# await hass.async_block_till_done() +# assert mock_update.call_count == 2 + +# # Throttled +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 2 + +# # Called from scheduler +# await self.advance(DEBOUNCE_TIMEOUT) +# await hass.async_block_till_done() +# assert mock_update.call_count == 3 + + +async def test_ignore_plex_web_client(hass): + """Test option to ignore Plex Web clients.""" + + OPTIONS = copy.deepcopy(DEFAULT_OPTIONS) + OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + server_id = mock_plex_server.machineIdentifier - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() - await self.advance(DEBOUNCE_TIMEOUT) - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == "0" + media_players = hass.states.async_entity_ids("media_player") - async def test_debouncer(self): - """Test debouncer behavior.""" - hass = self.hass - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - server_id = mock_plex_server.machineIdentifier - - with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( - mock_plex_server, "sessions", return_value=[] - ) as mock_update: - # Called immediately - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 - - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 - - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 - - # Called from scheduler - await self.advance(DEBOUNCE_TIMEOUT) - await hass.async_block_till_done() - assert mock_update.call_count == 2 - - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 2 - - # Called from scheduler - await self.advance(DEBOUNCE_TIMEOUT) - await hass.async_block_till_done() - assert mock_update.call_count == 3 + assert len(media_players) == int(sensor.state) - 1 diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index c1c705e752d..1714dd5a352 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -1,21 +1,20 @@ """Tests for the Point config flow.""" import asyncio -from unittest.mock import Mock, patch import pytest from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=protected-access - return_value=mock_coro("https://example.com"), side_effect=side_effect + flow._get_authorization_url = AsyncMock( # pylint: disable=protected-access + return_value="https://example.com", side_effect=side_effect ) flow.hass = hass return flow diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 8a559d1dd49..a384725daa8 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -3,7 +3,6 @@ import json import os -from asynctest import MagicMock, Mock from tesla_powerwall import ( DeviceType, GridStatus, @@ -17,6 +16,7 @@ from tesla_powerwall import ( from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS +from tests.async_mock import MagicMock, Mock from tests.common import load_fixture diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index c8a081de573..fcca7fb34ab 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -1,13 +1,13 @@ """The binary sensor tests for the powerwall platform.""" -from asynctest import patch - from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures +from tests.async_mock import patch + async def test_sensors(hass): """Test creation of the binary sensors.""" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index a6752d838f3..eaf53f0beef 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Powerwall config flow.""" -from asynctest import patch -from tesla_powerwall import PowerwallUnreachableError +from tesla_powerwall import APIChangedError, PowerwallUnreachableError from homeassistant import config_entries, setup from homeassistant.components.powerwall.const import DOMAIN @@ -9,6 +8,8 @@ from homeassistant.const import CONF_IP_ADDRESS from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name +from tests.async_mock import patch + async def test_form_source_user(hass): """Test we get config flow setup form as a user.""" @@ -86,3 +87,23 @@ async def test_form_cannot_connect(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_wrong_version(hass): + """Test we can handle wrong version error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerwall = _mock_powerwall_side_effect(site_info=APIChangedError(object, {})) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: "1.2.3.4"}, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "wrong_version"} diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index c68d9f0279e..af2835ea679 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -1,13 +1,13 @@ """The sensor tests for the powerwall platform.""" -from asynctest import patch - from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import UNIT_PERCENTAGE from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures +from tests.async_mock import patch + async def test_sensors(hass): """Test creation of the sensors.""" diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py new file mode 100644 index 00000000000..42002404db2 --- /dev/null +++ b/tests/components/ps4/conftest.py @@ -0,0 +1,23 @@ +"""Test configuration for PS4.""" +import pytest + +from tests.async_mock import patch + + +@pytest.fixture +def patch_load_json(): + """Prevent load JSON being used.""" + with patch("homeassistant.components.ps4.load_json", return_value={}) as mock_load: + yield mock_load + + +@pytest.fixture +def patch_save_json(): + """Prevent save JSON being used.""" + with patch("homeassistant.components.ps4.save_json") as mock_save: + yield mock_save + + +@pytest.fixture(autouse=True) +def patch_io(patch_load_json, patch_save_json): + """Prevent PS4 doing I/O.""" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 7c021199952..c5e0623de5b 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,7 +1,6 @@ """Define tests for the PlayStation 4 config flow.""" -from unittest.mock import patch - from pyps4_2ndscreen.errors import CredentialTimeout +import pytest from homeassistant import data_entry_flow from homeassistant.components import ps4 @@ -21,7 +20,8 @@ from homeassistant.const import ( ) from homeassistant.util import location -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry MOCK_TITLE = "PlayStation 4" MOCK_CODE = 12345678 @@ -74,22 +74,40 @@ MOCK_LOCATION = location.LocationInfo( ) +@pytest.fixture(name="location_info", autouse=True) +def location_info_fixture(): + """Mock location info.""" + with patch( + "homeassistant.components.ps4.config_flow.location.async_detect_location_info", + return_value=MOCK_LOCATION, + ): + yield + + +@pytest.fixture(name="ps4_setup", autouse=True) +def ps4_setup_fixture(): + """Patch ps4 setup entry.""" + with patch( + "homeassistant.components.ps4.async_setup_entry", return_value=True, + ): + yield + + async def test_full_flow_implementation(hass): """Test registering an implementation and flow works.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.location = MOCK_LOCATION - manager = hass.config_entries - # User Step Started, results in Step Creds with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): - result = await flow.async_step_creds({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" @@ -97,7 +115,9 @@ async def test_full_flow_implementation(hass): with patch( "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): - result = await flow.async_step_mode(MOCK_AUTO) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" @@ -105,44 +125,30 @@ async def test_full_flow_implementation(hass): with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): - result = await flow.async_step_link(MOCK_CONFIG) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_CONFIG + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE - await hass.async_block_till_done() - - # Add entry using result data. - mock_data = { - CONF_TOKEN: result["data"][CONF_TOKEN], - "devices": result["data"]["devices"], - } - entry = MockConfigEntry(domain=ps4.DOMAIN, data=mock_data) - entry.add_to_manager(manager) - - # Check if entry exists. - assert len(manager.async_entries()) == 1 - # Check if there is a device config in entry. - assert len(entry.data["devices"]) == 1 - async def test_multiple_flow_implementation(hass): """Test multiple device flows.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.location = MOCK_LOCATION - manager = hass.config_entries - # User Step Started, results in Step Creds with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): - result = await flow.async_step_creds({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" @@ -151,7 +157,9 @@ async def test_multiple_flow_implementation(hass): "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): - result = await flow.async_step_mode(MOCK_AUTO) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" @@ -160,26 +168,20 @@ async def test_multiple_flow_implementation(hass): "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): - result = await flow.async_step_link(MOCK_CONFIG) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_CONFIG + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE - await hass.async_block_till_done() - - # Add entry using result data. - mock_data = { - CONF_TOKEN: result["data"][CONF_TOKEN], - "devices": result["data"]["devices"], - } - entry = MockConfigEntry(domain=ps4.DOMAIN, data=mock_data) - entry.add_to_manager(manager) - # Check if entry exists. - assert len(manager.async_entries()) == 1 + entries = hass.config_entries.async_entries() + assert len(entries) == 1 # Check if there is a device config in entry. - assert len(entry.data["devices"]) == 1 + entry_1 = entries[0] + assert len(entry_1.data["devices"]) == 1 # Test additional flow. @@ -188,13 +190,17 @@ async def test_multiple_flow_implementation(hass): "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): - result = await flow.async_step_creds({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" @@ -203,7 +209,9 @@ async def test_multiple_flow_implementation(hass): "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): - result = await flow.async_step_mode(MOCK_AUTO) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" @@ -212,44 +220,40 @@ async def test_multiple_flow_implementation(hass): "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): - result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE - await hass.async_block_till_done() - - mock_data = { - CONF_TOKEN: result["data"][CONF_TOKEN], - "devices": result["data"]["devices"], - } - - # Update config entries with result data - entry = MockConfigEntry(domain=ps4.DOMAIN, data=mock_data) - entry.add_to_manager(manager) - manager.async_update_entry(entry) - # Check if there are 2 entries. - assert len(manager.async_entries()) == 2 - # Check if there is device config in entry. - assert len(entry.data["devices"]) == 1 + entries = hass.config_entries.async_entries() + assert len(entries) == 2 + # Check if there is device config in the last entry. + entry_2 = entries[-1] + assert len(entry_2.data["devices"]) == 1 + + # Check that entry 1 is different from entry 2. + assert entry_1 is not entry_2 async def test_port_bind_abort(hass): """Test that flow aborted when cannot bind to ports 987, 997.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_UDP_PORT): reason = "port_987_bind_error" - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): reason = "port_997_bind_error" - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason @@ -257,48 +261,69 @@ async def test_port_bind_abort(hass): async def test_duplicate_abort(hass): """Test that Flow aborts when found devices already configured.""" MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA).add_to_hass(hass) - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.creds = MOCK_CREDS + + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" + + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" with patch( "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): - result = await flow.async_step_link(user_input=None) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "devices_configured" async def test_additional_device(hass): """Test that Flow can configure another device.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.creds = MOCK_CREDS - manager = hass.config_entries - # Mock existing entry. entry = MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA) - entry.add_to_manager(manager) - # Check that only 1 entry exists - assert len(manager.async_entries()) == 1 + entry.add_to_hass(hass) + + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" + + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" with patch( "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): - result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) + + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE - # Add New Entry - entry = MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA) - entry.add_to_manager(manager) - - # Check that there are 2 entries - assert len(manager.async_entries()) == 2 - async def test_0_pin(hass): """Test Pin with leading '0' is passed correctly.""" @@ -313,7 +338,7 @@ async def test_0_pin(hass): "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ), patch( "homeassistant.components.ps4.config_flow.location.async_detect_location_info", - return_value=mock_coro(MOCK_LOCATION), + return_value=MOCK_LOCATION, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO @@ -338,63 +363,121 @@ async def test_0_pin(hass): async def test_no_devices_found_abort(hass): """Test that failure to find devices aborts flow.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" + + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): - result = await flow.async_step_link() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" async def test_manual_mode(hass): """Test host specified in manual mode is passed to Step Link.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.location = MOCK_LOCATION + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" + + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" # Step Mode with User Input: manual, results in Step Link. with patch( - "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": flow.m_device}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): - result = await flow.async_step_mode(MOCK_MANUAL) - assert flow.m_device == MOCK_HOST + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_MANUAL + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" async def test_credential_abort(hass): """Test that failure to get credentials aborts flow.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): - result = await flow.async_step_creds({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "credential_error" async def test_credential_timeout(hass): """Test that Credential Timeout shows error.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): - result = await flow.async_step_creds({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" assert result["errors"] == {"base": "credential_timeout"} async def test_wrong_pin_error(hass): """Test that incorrect pin throws an error.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.location = MOCK_LOCATION + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" - with patch("pyps4_2ndscreen.Helper.link", return_value=(True, False)), patch( + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" + + with patch( "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): - result = await flow.async_step_link(MOCK_CONFIG) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) + + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, False)): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_CONFIG + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "login_failed"} @@ -402,14 +485,31 @@ async def test_wrong_pin_error(hass): async def test_device_connection_error(hass): """Test that device not connected or on throws an error.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass - flow.location = MOCK_LOCATION + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" - with patch("pyps4_2ndscreen.Helper.link", return_value=(False, True)), patch( + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" + + with patch( "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): - result = await flow.async_step_link(MOCK_CONFIG) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_AUTO + ) + + with patch("pyps4_2ndscreen.Helper.link", return_value=(False, True)): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_CONFIG + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "not_ready"} @@ -417,12 +517,24 @@ async def test_device_connection_error(hass): async def test_manual_mode_no_ip_error(hass): """Test no IP specified in manual mode throws an error.""" - flow = ps4.PlayStation4FlowHandler() - flow.hass = hass + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "creds" - mock_input = {"Config Mode": "Manual Entry"} + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"Config Mode": "Manual Entry"} + ) - result = await flow.async_step_mode(mock_input) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" assert result["errors"] == {CONF_IP_ADDRESS: "no_ipaddress"} diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 453202b6c67..7f7eff33ebb 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -1,6 +1,4 @@ """Tests for the PS4 Integration.""" -from unittest.mock import MagicMock, patch - from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -29,7 +27,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.common import MockConfigEntry, mock_coro, mock_registry +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, mock_registry MOCK_HOST = "192.168.0.1" MOCK_NAME = "test_ps4" @@ -119,8 +118,8 @@ async def test_creating_entry_sets_up_media_player(hass): mock_flow = "homeassistant.components.ps4.PlayStation4FlowHandler.async_step_user" with patch( "homeassistant.components.ps4.media_player.async_setup_entry", - return_value=mock_coro(True), - ) as mock_setup, patch(mock_flow, return_value=mock_coro(MOCK_FLOW_RESULT)): + return_value=True, + ) as mock_setup, patch(mock_flow, return_value=MOCK_FLOW_RESULT): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -152,10 +151,10 @@ async def test_config_flow_entry_migrate(hass): with patch( "homeassistant.util.location.async_detect_location_info", - return_value=mock_coro(MOCK_LOCATION), + return_value=MOCK_LOCATION, ), patch( "homeassistant.helpers.entity_registry.async_get_registry", - return_value=mock_coro(mock_e_registry), + return_value=mock_e_registry, ): await ps4.async_migrate_entry(hass, mock_entry) @@ -281,7 +280,7 @@ async def test_send_command(hass): assert mock_entity.entity_id == f"media_player.{MOCK_NAME}" # Test that all commands call service function. - with patch(mock_func, return_value=mock_coro(True)) as mock_service: + with patch(mock_func, return_value=True) as mock_service: for mock_command in COMMANDS: await hass.services.async_call( DOMAIN, diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 3bd75c40bed..8ff8dea71ce 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,6 +1,4 @@ """Tests for the PS4 media player platform.""" -from unittest.mock import MagicMock, patch - from pyps4_2ndscreen.credential import get_ddp_message from homeassistant.components import ps4 @@ -35,7 +33,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro, mock_device_registry, mock_registry +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, mock_device_registry, mock_registry MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd" MOCK_NAME = "ha_ps4_name" @@ -123,7 +122,6 @@ MOCK_DATA = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE]} MOCK_CONFIG = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY_ID) MOCK_LOAD = "homeassistant.components.ps4.media_player.load_games" -MOCK_SAVE = "homeassistant.components.ps4.save_json" async def setup_mock_component(hass, entry=None): @@ -137,9 +135,7 @@ async def setup_mock_component(hass, entry=None): mock_entry.add_to_hass(hass) - # Don't use an actual file. - with patch(MOCK_LOAD, return_value={}), patch(MOCK_SAVE, side_effect=MagicMock()): - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -150,7 +146,7 @@ async def setup_mock_component(hass, entry=None): return mock_entity_id -async def mock_ddp_response(hass, mock_status_data, games=None): +async def mock_ddp_response(hass, mock_status_data): """Mock raw UDP response from device.""" mock_protocol = hass.data[PS4_DATA].protocol @@ -159,14 +155,8 @@ async def mock_ddp_response(hass, mock_status_data, games=None): mock_status_header = f"{mock_code} {mock_status}" mock_response = get_ddp_message(mock_status_header, mock_status_data).encode() - if games is None: - games = {} - - with patch(MOCK_LOAD, return_value=games), patch( - MOCK_SAVE, side_effect=MagicMock() - ): - mock_protocol.datagram_received(mock_response, (MOCK_HOST, MOCK_RANDOM_PORT)) - await hass.async_block_till_done() + mock_protocol.datagram_received(mock_response, (MOCK_HOST, MOCK_RANDOM_PORT)) + await hass.async_block_till_done() async def test_media_player_is_setup_correctly_with_entry(hass): @@ -187,8 +177,7 @@ async def test_state_standby_is_set(hass): """Test that state is set to standby.""" mock_entity_id = await setup_mock_component(hass) - with patch(MOCK_SAVE, side_effect=MagicMock()): - await mock_ddp_response(hass, MOCK_STATUS_STANDBY) + await mock_ddp_response(hass, MOCK_STATUS_STANDBY) assert hass.states.get(mock_entity_id).state == STATE_STANDBY @@ -201,9 +190,7 @@ async def test_state_playing_is_set(hass): "pyps4.Ps4Async.async_get_ps_store_data", ) - with patch(mock_func, return_value=mock_coro(None)), patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch(mock_func, return_value=None): await mock_ddp_response(hass, MOCK_STATUS_PLAYING) assert hass.states.get(mock_entity_id).state == STATE_PLAYING @@ -213,8 +200,7 @@ async def test_state_idle_is_set(hass): """Test that state is set to idle.""" mock_entity_id = await setup_mock_component(hass) - with patch(MOCK_SAVE, side_effect=MagicMock()): - await mock_ddp_response(hass, MOCK_STATUS_IDLE) + await mock_ddp_response(hass, MOCK_STATUS_IDLE) assert hass.states.get(mock_entity_id).state == STATE_IDLE @@ -240,9 +226,7 @@ async def test_media_attributes_are_fetched(hass): mock_result.cover_art = MOCK_TITLE_ART_URL mock_result.game_type = "game" - with patch(mock_func, return_value=mock_coro(mock_result)) as mock_fetch, patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch(mock_func, return_value=mock_result) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) mock_state = hass.states.get(mock_entity_id) @@ -258,19 +242,17 @@ async def test_media_attributes_are_fetched(hass): assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE -async def test_media_attributes_are_loaded(hass): +async def test_media_attributes_are_loaded(hass, patch_load_json): """Test that media attributes are loaded.""" mock_entity_id = await setup_mock_component(hass) - mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA_LOCKED} - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", - "pyps4.Ps4Async.async_get_ps_store_data", - ) + patch_load_json.return_value = {MOCK_TITLE_ID: MOCK_GAMES_DATA_LOCKED} - with patch(mock_func, return_value=mock_coro(None)) as mock_fetch, patch( - MOCK_SAVE, side_effect=MagicMock() - ): - await mock_ddp_response(hass, MOCK_STATUS_PLAYING, mock_data) + with patch( + "homeassistant.components.ps4.media_player." + "pyps4.Ps4Async.async_get_ps_store_data", + return_value=None, + ) as mock_fetch: + await mock_ddp_response(hass, MOCK_STATUS_PLAYING) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) @@ -372,9 +354,7 @@ async def test_turn_on(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.wakeup" ) - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch(mock_func) as mock_call: await hass.services.async_call( "media_player", "turn_on", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -390,9 +370,7 @@ async def test_turn_off(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.standby" ) - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch(mock_func) as mock_call: await hass.services.async_call( "media_player", "turn_off", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -408,9 +386,7 @@ async def test_media_pause(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch(mock_func) as mock_call: await hass.services.async_call( "media_player", "media_pause", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -426,9 +402,7 @@ async def test_media_stop(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch(mock_func) as mock_call: await hass.services.async_call( "media_player", "media_stop", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -437,46 +411,34 @@ async def test_media_stop(hass): assert len(mock_call.mock_calls) == 1 -async def test_select_source(hass): +async def test_select_source(hass, patch_load_json): """Test that select source service calls function with title.""" - mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( - MOCK_LOAD, return_value=mock_data - ): + patch_load_json.return_value = {MOCK_TITLE_ID: MOCK_GAMES_DATA} + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE): mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" - ) - - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() + with patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, patch( + "homeassistant.components.ps4.media_player.PS4Device.async_update" ): # Test with title name. await hass.services.async_call( "media_player", "select_source", {ATTR_ENTITY_ID: mock_entity_id, ATTR_INPUT_SOURCE: MOCK_TITLE_NAME}, + blocking=True, ) - await hass.async_block_till_done() assert len(mock_call.mock_calls) == 1 -async def test_select_source_caps(hass): +async def test_select_source_caps(hass, patch_load_json): """Test that select source service calls function with upper case title.""" - mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( - MOCK_LOAD, return_value=mock_data - ): + patch_load_json.return_value = {MOCK_TITLE_ID: MOCK_GAMES_DATA} + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE): mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" - ) - - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() + with patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, patch( + "homeassistant.components.ps4.media_player.PS4Device.async_update" ): # Test with title name in caps. await hass.services.async_call( @@ -486,34 +448,28 @@ async def test_select_source_caps(hass): ATTR_ENTITY_ID: mock_entity_id, ATTR_INPUT_SOURCE: MOCK_TITLE_NAME.upper(), }, + blocking=True, ) - await hass.async_block_till_done() assert len(mock_call.mock_calls) == 1 -async def test_select_source_id(hass): +async def test_select_source_id(hass, patch_load_json): """Test that select source service calls function with Title ID.""" - mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( - MOCK_LOAD, return_value=mock_data - ): + patch_load_json.return_value = {MOCK_TITLE_ID: MOCK_GAMES_DATA} + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE): mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" - ) - - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() + with patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, patch( + "homeassistant.components.ps4.media_player.PS4Device.async_update" ): # Test with title ID. await hass.services.async_call( "media_player", "select_source", {ATTR_ENTITY_ID: mock_entity_id, ATTR_INPUT_SOURCE: MOCK_TITLE_ID}, + blocking=True, ) - await hass.async_block_till_done() assert len(mock_call.mock_calls) == 1 @@ -521,17 +477,14 @@ async def test_select_source_id(hass): async def test_ps4_send_command(hass): """Test that ps4 send command service calls function.""" mock_entity_id = await setup_mock_component(hass) - mock_func = "{}{}".format( - "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" - ) - with patch(mock_func, return_value=MagicMock()) as mock_call, patch( - MOCK_SAVE, side_effect=MagicMock() - ): + with patch("pyps4_2ndscreen.ps4.Ps4Async.remote_control") as mock_call: await hass.services.async_call( - DOMAIN, "send_command", {ATTR_ENTITY_ID: mock_entity_id, ATTR_COMMAND: "ps"} + DOMAIN, + "send_command", + {ATTR_ENTITY_ID: mock_entity_id, ATTR_COMMAND: "ps"}, + blocking=True, ) - await hass.async_block_till_done() assert len(mock_call.mock_calls) == 1 diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py index d4a2aa1ab94..93e1bb540db 100644 --- a/tests/components/ptvsd/test_ptvsd.py +++ b/tests/components/ptvsd/test_ptvsd.py @@ -1,14 +1,13 @@ """Tests for PTVSD Debugger.""" -from unittest.mock import patch - -from asynctest import CoroutineMock from pytest import mark from homeassistant.bootstrap import _async_set_up_integrations import homeassistant.components.ptvsd as ptvsd_component from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, patch + @mark.skip("causes code cover to fail") async def test_ptvsd(hass): @@ -42,9 +41,7 @@ async def test_ptvsd_bootstrap(hass): """Test loading ptvsd component with wait.""" config = {ptvsd_component.DOMAIN: {ptvsd_component.CONF_WAIT: True}} - with patch( - "homeassistant.components.ptvsd.async_setup", CoroutineMock() - ) as setup_mock: + with patch("homeassistant.components.ptvsd.async_setup", AsyncMock()) as setup_mock: setup_mock.return_value = True await _async_set_up_integrations(hass, config) diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index b5803b96889..8f4bb43045e 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -3,12 +3,17 @@ from datetime import timedelta import io from homeassistant import core as ha +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util async def test_bad_posting(hass, aiohttp_client): """Test that posting to wrong api endpoint fails.""" + await async_process_ha_core_config( + hass, {"external_url": "http://example.com"}, + ) + await async_setup_component( hass, "camera", @@ -35,6 +40,10 @@ async def test_bad_posting(hass, aiohttp_client): async def test_posting_url(hass, aiohttp_client): """Test that posting to api endpoint works.""" + await async_process_ha_core_config( + hass, {"external_url": "http://example.com"}, + ) + await async_setup_component( hass, "camera", diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index 4c731c1f704..930d9261f9c 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,7 +1,6 @@ """The tests for the pushbullet notification platform.""" import json import unittest -from unittest.mock import patch from pushbullet import PushBullet import requests_mock @@ -9,6 +8,7 @@ import requests_mock import homeassistant.components.notify as notify from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index fbbe87fee5f..d76f74f64a1 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the pvpc_hourly_pricing config_flow.""" from datetime import datetime -from unittest.mock import patch from pytz import timezone @@ -11,6 +10,7 @@ from homeassistant.helpers import entity_registry from .conftest import check_valid_state +from tests.async_mock import patch from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index fdab7fd1008..7ef50113de5 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -1,7 +1,6 @@ """Tests for the pvpc_hourly_pricing sensor component.""" from datetime import datetime, timedelta import logging -from unittest.mock import patch from pytz import timezone @@ -11,6 +10,7 @@ from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED from .conftest import check_valid_state +from tests.async_mock import patch from tests.common import async_setup_component, date_util from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index d122fef0bd2..90d2ac67faf 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,11 +1,11 @@ """Test the python_script component.""" import logging -from unittest.mock import mock_open, patch from homeassistant.components.python_script import DOMAIN, FOLDER, execute from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component +from tests.async_mock import mock_open, patch from tests.common import patch_yaml_files diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index afcd2f11802..15518afbc2d 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the Queensland Bushfire Alert Feed platform.""" import datetime -from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -28,6 +27,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "qld_bushfire", CONF_RADIUS: 200}]} diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 93e8e3fe8df..6075174fa98 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -2,70 +2,83 @@ import asyncio import logging +from aiohttp.client_exceptions import ClientError +import pytest +from yarl import URL + from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH -from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from tests.test_util.aiohttp import MockLongPollSideEffect +from tests.async_mock import Mock +from tests.test_util.aiohttp import AiohttpClientMockResponse, MockLongPollSideEffect _LOGGER = logging.getLogger(__name__) -DEVICES = [ - { - "id": "@000001", - "name": "Switch 1", - "type": "rel", - "val": "OFF", - "time": "1522777506", - "rssi": "51%", - }, - { - "id": "@000002", - "name": "Light 2", - "type": "rel", - "val": "ON", - "time": "1522777507", - "rssi": "45%", - }, - { - "id": "@000003", - "name": "Dim 3", - "type": "dim", - "val": "280c00", - "time": "1522777544", - "rssi": "62%", - }, -] +@pytest.fixture +def qs_devices(): + """Return a set of devices as a response.""" + return [ + { + "id": "@a00001", + "name": "Switch 1", + "type": "rel", + "val": "OFF", + "time": "1522777506", + "rssi": "51%", + }, + { + "id": "@a00002", + "name": "Light 2", + "type": "rel", + "val": "ON", + "time": "1522777507", + "rssi": "45%", + }, + { + "id": "@a00003", + "name": "Dim 3", + "type": "dim", + "val": "280c00", + "time": "1522777544", + "rssi": "62%", + }, + ] -async def test_binary_sensor_device(hass, aioclient_mock): +EMPTY_PACKET = {"cmd": ""} + + +async def test_binary_sensor_device(hass, aioclient_mock, qs_devices): """Test a binary sensor device.""" config = { "qwikswitch": { "sensors": {"name": "s1", "id": "@a00001", "channel": 1, "type": "imod"} } } - aioclient_mock.get("http://127.0.0.1:2020/&device", json=DEVICES) + aioclient_mock.get("http://127.0.0.1:2020/&device", json=qs_devices) listen_mock = MockLongPollSideEffect() aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) - await async_setup_component(hass, QWIKSWITCH, config) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() await hass.async_block_till_done() + # verify initial state is off per the 'val' in qs_devices state_obj = hass.states.get("binary_sensor.s1") assert state_obj.state == "off" + # receive turn on command from network listen_mock.queue_response( - json={"id": "@a00001", "cmd": "", "data": "4e0e1601", "rssi": "61%"} + json={"id": "@a00001", "cmd": "STATUS.ACK", "data": "4e0e1601", "rssi": "61%"} ) await asyncio.sleep(0.01) await hass.async_block_till_done() state_obj = hass.states.get("binary_sensor.s1") assert state_obj.state == "on" + # receive turn off command from network listen_mock.queue_response( - json={"id": "@a00001", "cmd": "", "data": "4e0e1701", "rssi": "61%"}, + json={"id": "@a00001", "cmd": "STATUS.ACK", "data": "4e0e1701", "rssi": "61%"}, ) await asyncio.sleep(0.01) await hass.async_block_till_done() @@ -75,7 +88,7 @@ async def test_binary_sensor_device(hass, aioclient_mock): listen_mock.stop() -async def test_sensor_device(hass, aioclient_mock): +async def test_sensor_device(hass, aioclient_mock, qs_devices): """Test a sensor device.""" config = { "qwikswitch": { @@ -87,16 +100,17 @@ async def test_sensor_device(hass, aioclient_mock): } } } - aioclient_mock.get("http://127.0.0.1:2020/&device", json=DEVICES) + aioclient_mock.get("http://127.0.0.1:2020/&device", json=qs_devices) listen_mock = MockLongPollSideEffect() aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) - await async_setup_component(hass, QWIKSWITCH, config) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() await hass.async_block_till_done() state_obj = hass.states.get("sensor.ss1") assert state_obj.state == "None" + # receive command that sets the sensor value listen_mock.queue_response( json={"id": "@a00001", "name": "ss1", "type": "rel", "val": "4733800001a00000"}, ) @@ -106,3 +120,299 @@ async def test_sensor_device(hass, aioclient_mock): assert state_obj.state == "416" listen_mock.stop() + + +async def test_switch_device(hass, aioclient_mock, qs_devices): + """Test a switch device.""" + + async def get_devices_json(method, url, data): + return AiohttpClientMockResponse(method=method, url=url, json=qs_devices) + + config = {"qwikswitch": {"switches": ["@a00001"]}} + aioclient_mock.get("http://127.0.0.1:2020/&device", side_effect=get_devices_json) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + + # verify initial state is off per the 'val' in qs_devices + state_obj = hass.states.get("switch.switch_1") + assert state_obj.state == "off" + + # ask hass to turn on and verify command is sent to device + aioclient_mock.mock_calls.clear() + aioclient_mock.get("http://127.0.0.1:2020/@a00001=100", json={"data": "OK"}) + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.switch_1"}, blocking=True + ) + await asyncio.sleep(0.01) + assert ( + "GET", + URL("http://127.0.0.1:2020/@a00001=100"), + None, + None, + ) in aioclient_mock.mock_calls + # verify state is on + state_obj = hass.states.get("switch.switch_1") + assert state_obj.state == "on" + + # ask hass to turn off and verify command is sent to device + aioclient_mock.mock_calls.clear() + aioclient_mock.get("http://127.0.0.1:2020/@a00001=0", json={"data": "OK"}) + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.switch_1"}, blocking=True + ) + assert ( + "GET", + URL("http://127.0.0.1:2020/@a00001=0"), + None, + None, + ) in aioclient_mock.mock_calls + # verify state is off + state_obj = hass.states.get("switch.switch_1") + assert state_obj.state == "off" + + # check if setting the value in the network show in hass + qs_devices[0]["val"] = "ON" + listen_mock.queue_response(json=EMPTY_PACKET) + await hass.async_block_till_done() + state_obj = hass.states.get("switch.switch_1") + assert state_obj.state == "on" + + listen_mock.stop() + + +async def test_light_device(hass, aioclient_mock, qs_devices): + """Test a light device.""" + + async def get_devices_json(method, url, data): + return AiohttpClientMockResponse(method=method, url=url, json=qs_devices) + + config = {"qwikswitch": {}} + aioclient_mock.get("http://127.0.0.1:2020/&device", side_effect=get_devices_json) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + + # verify initial state is on per the 'val' in qs_devices + state_obj = hass.states.get("light.dim_3") + assert state_obj.state == "on" + assert state_obj.attributes["brightness"] == 255 + + # ask hass to turn off and verify command is sent to device + aioclient_mock.mock_calls.clear() + aioclient_mock.get("http://127.0.0.1:2020/@a00003=0", json={"data": "OK"}) + await hass.services.async_call( + "light", "turn_off", {"entity_id": "light.dim_3"}, blocking=True + ) + await asyncio.sleep(0.01) + assert ( + "GET", + URL("http://127.0.0.1:2020/@a00003=0"), + None, + None, + ) in aioclient_mock.mock_calls + state_obj = hass.states.get("light.dim_3") + assert state_obj.state == "off" + + # change brightness in network and check that hass updates + qs_devices[2]["val"] = "280c55" # half dimmed + listen_mock.queue_response(json=EMPTY_PACKET) + await asyncio.sleep(0.01) + await hass.async_block_till_done() + state_obj = hass.states.get("light.dim_3") + assert state_obj.state == "on" + assert 16 < state_obj.attributes["brightness"] < 240 + + # turn off in the network and see that it is off in hass as well + qs_devices[2]["val"] = "280c78" # off + listen_mock.queue_response(json=EMPTY_PACKET) + await asyncio.sleep(0.01) + await hass.async_block_till_done() + state_obj = hass.states.get("light.dim_3") + assert state_obj.state == "off" + + # ask hass to turn on and verify command is sent to device + aioclient_mock.mock_calls.clear() + aioclient_mock.get("http://127.0.0.1:2020/@a00003=100", json={"data": "OK"}) + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.dim_3"}, blocking=True + ) + assert ( + "GET", + URL("http://127.0.0.1:2020/@a00003=100"), + None, + None, + ) in aioclient_mock.mock_calls + await hass.async_block_till_done() + state_obj = hass.states.get("light.dim_3") + assert state_obj.state == "on" + + listen_mock.stop() + + +async def test_button(hass, aioclient_mock, qs_devices): + """Test that buttons fire an event.""" + + async def get_devices_json(method, url, data): + return AiohttpClientMockResponse(method=method, url=url, json=qs_devices) + + config = {"qwikswitch": {"button_events": "TOGGLE"}} + aioclient_mock.get("http://127.0.0.1:2020/&device", side_effect=get_devices_json) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + + button_pressed = Mock() + hass.bus.async_listen_once("qwikswitch.button.@a00002", button_pressed) + listen_mock.queue_response(json={"id": "@a00002", "cmd": "TOGGLE"},) + await asyncio.sleep(0.01) + await hass.async_block_till_done() + button_pressed.assert_called_once() + + listen_mock.stop() + + +async def test_failed_update_devices(hass, aioclient_mock): + """Test that code behaves correctly when unable to get the devices.""" + + config = {"qwikswitch": {}} + aioclient_mock.get("http://127.0.0.1:2020/&device", exc=ClientError()) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert not await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + listen_mock.stop() + + +async def test_single_invalid_sensor(hass, aioclient_mock, qs_devices): + """Test that a single misconfigured sensor doesn't block the others.""" + + config = { + "qwikswitch": { + "sensors": [ + {"name": "ss1", "id": "@a00001", "channel": 1, "type": "qwikcord"}, + {"name": "ss2", "id": "@a00002", "channel": 1, "type": "ERROR_TYPE"}, + {"name": "ss3", "id": "@a00003", "channel": 1, "type": "qwikcord"}, + ] + } + } + aioclient_mock.get("http://127.0.0.1:2020/&device", json=qs_devices) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + await asyncio.sleep(0.01) + assert hass.states.get("sensor.ss1") + assert not hass.states.get("sensor.ss2") + assert hass.states.get("sensor.ss3") + listen_mock.stop() + + +async def test_non_binary_sensor_with_binary_args( + hass, aioclient_mock, qs_devices, caplog +): + """Test that the system logs a warning when a non-binary device has binary specific args.""" + + config = { + "qwikswitch": { + "sensors": [ + { + "name": "ss1", + "id": "@a00001", + "channel": 1, + "type": "qwikcord", + "invert": True, + }, + ] + } + } + aioclient_mock.get("http://127.0.0.1:2020/&device", json=qs_devices) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + await asyncio.sleep(0.01) + await hass.async_block_till_done() + assert hass.states.get("sensor.ss1") + assert "invert should only be used for binary_sensors" in caplog.text + listen_mock.stop() + + +async def test_non_relay_switch(hass, aioclient_mock, qs_devices, caplog): + """Test that the system logs a warning when a switch is configured for a device that is not a relay.""" + + config = {"qwikswitch": {"switches": ["@a00003"]}} + aioclient_mock.get("http://127.0.0.1:2020/&device", json=qs_devices) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + await asyncio.sleep(0.01) + await hass.async_block_till_done() + assert not hass.states.get("switch.dim_3") + assert "You specified a switch that is not a relay @a00003" in caplog.text + listen_mock.stop() + + +async def test_unknown_device(hass, aioclient_mock, qs_devices, caplog): + """Test that the system logs a warning when a network device has unknown type.""" + + config = {"qwikswitch": {}} + qs_devices[1]["type"] = "ERROR_TYPE" + aioclient_mock.get("http://127.0.0.1:2020/&device", json=qs_devices) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_start() + await hass.async_block_till_done() + await asyncio.sleep(0.01) + await hass.async_block_till_done() + assert hass.states.get("light.switch_1") + assert not hass.states.get("light.light_2") + assert hass.states.get("light.dim_3") + assert "Ignored unknown QSUSB device" in caplog.text + listen_mock.stop() + + +async def test_no_discover_info(hass, hass_storage, aioclient_mock, caplog): + """Test that discovery with no discovery_info does not result in errors.""" + config = { + "qwikswitch": {}, + "light": {"platform": "qwikswitch"}, + "switch": {"platform": "qwikswitch"}, + "sensor": {"platform": "qwikswitch"}, + "binary_sensor": {"platform": "qwikswitch"}, + } + aioclient_mock.get( + "http://127.0.0.1:2020/&device", + json=[ + { + "id": "@a00001", + "name": "Switch 1", + "type": "ERROR_TYPE", + "val": "OFF", + "time": "1522777506", + "rssi": "51%", + }, + ], + ) + listen_mock = MockLongPollSideEffect() + aioclient_mock.get("http://127.0.0.1:2020/&listen", side_effect=listen_mock) + assert await async_setup_component(hass, "light", config) + assert await async_setup_component(hass, "switch", config) + assert await async_setup_component(hass, "sensor", config) + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + assert "Error while setting up qwikswitch platform" not in caplog.text + listen_mock.stop() diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 57575fe5501..7cc3f272e7a 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -1,7 +1,4 @@ """Test the Rachio config flow.""" -from asynctest import patch -from asynctest.mock import MagicMock - from homeassistant import config_entries, setup from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, @@ -10,6 +7,7 @@ from homeassistant.components.rachio.const import ( ) from homeassistant.const import CONF_API_KEY +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry @@ -111,16 +109,26 @@ async def test_form_homekit(hass): await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "homekit"} + DOMAIN, + context={"source": "homekit"}, + data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "form" assert result["errors"] == {} + flow = next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF" entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "api_key"}) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "homekit"} + DOMAIN, + context={"source": "homekit"}, + data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index c18476a92a9..0e76e99e721 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -6,6 +6,7 @@ import pytest import homeassistant.components.radarr.sensor as radarr from homeassistant.const import DATA_GIGABYTES +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -212,7 +213,7 @@ class TestRadarrSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_diskspace_no_paths(self, req_mock): """Test getting all disk space.""" config = { @@ -232,7 +233,7 @@ class TestRadarrSetup(unittest.TestCase): assert "Radarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_diskspace_paths(self, req_mock): """Test getting diskspace for included paths.""" config = { @@ -252,7 +253,7 @@ class TestRadarrSetup(unittest.TestCase): assert "Radarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_commands(self, req_mock): """Test getting running commands.""" config = { @@ -272,7 +273,7 @@ class TestRadarrSetup(unittest.TestCase): assert "Radarr Commands" == device.name assert "pending" == device.device_state_attributes["RescanMovie"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_movies(self, req_mock): """Test getting the number of movies.""" config = { @@ -292,7 +293,7 @@ class TestRadarrSetup(unittest.TestCase): assert "Radarr Movies" == device.name assert "false" == device.device_state_attributes["Assassin's Creed (2016)"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_upcoming_multiple_days(self, req_mock): """Test the upcoming movies for multiple days.""" config = { @@ -316,7 +317,7 @@ class TestRadarrSetup(unittest.TestCase): ) @pytest.mark.skip - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_upcoming_today(self, req_mock): """Test filtering for a single day. @@ -342,7 +343,7 @@ class TestRadarrSetup(unittest.TestCase): == device.device_state_attributes["Resident Evil (2017)"] ) - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_system_status(self, req_mock): """Test the getting of the system status.""" config = { @@ -362,7 +363,7 @@ class TestRadarrSetup(unittest.TestCase): assert "4.8.13.1" == device.device_state_attributes["osVersion"] @pytest.mark.skip - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_ssl(self, req_mock): """Test SSL being enabled.""" config = { @@ -387,7 +388,7 @@ class TestRadarrSetup(unittest.TestCase): == device.device_state_attributes["Resident Evil (2017)"] ) - @unittest.mock.patch("requests.get", side_effect=mocked_exception) + @patch("requests.get", side_effect=mocked_exception) def test_exception_handling(self, req_mock): """Test exception being handled.""" config = { diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index fca0f624a29..04dc67bdbe8 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,6 +1,4 @@ """Define tests for the OpenUV config flow.""" -from unittest.mock import patch - from regenmaschine.errors import RainMachineError from homeassistant import data_entry_flow @@ -14,7 +12,8 @@ from homeassistant.const import ( CONF_SSL, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_duplicate_error(hass): @@ -52,7 +51,7 @@ async def test_invalid_password(hass): with patch( "homeassistant.components.rainmachine.config_flow.login", - return_value=mock_coro(exception=RainMachineError), + side_effect=RainMachineError, ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -85,8 +84,7 @@ async def test_step_import(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", - return_value=mock_coro(True), + "homeassistant.components.rainmachine.config_flow.login", return_value=True, ): result = await flow.async_step_import(import_config=conf) @@ -117,8 +115,7 @@ async def test_step_user(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", - return_value=mock_coro(True), + "homeassistant.components.rainmachine.config_flow.login", return_value=True, ): result = await flow.async_step_user(user_input=conf) diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index a11b571dd83..975da102ca6 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -1,9 +1,9 @@ """The test for the Random binary sensor platform.""" import unittest -from unittest.mock import patch from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 34e0231d75a..1931a367ee8 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from datetime import datetime, timedelta import unittest -from unittest.mock import patch import pytest @@ -17,6 +16,7 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done +from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d10dad43d75..d3cf69fc994 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,7 +1,4 @@ """The tests for the Recorder component.""" -# pylint: disable=protected-access -from unittest.mock import call, patch - import pytest from sqlalchemy import create_engine from sqlalchemy.pool import StaticPool @@ -9,6 +6,8 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components.recorder import const, migration, models +# pylint: disable=protected-access +from tests.async_mock import call, patch from tests.components.recorder import models_original diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index e0993b8cffc..4ec08c432b0 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta import json import unittest -from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE @@ -10,6 +9,7 @@ from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.purge import purge_old_data from homeassistant.components.recorder.util import session_scope +from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index eca146f3efa..8de5acd78db 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1,11 +1,10 @@ """Test util methods.""" -from unittest.mock import MagicMock, patch - import pytest from homeassistant.components.recorder import util from homeassistant.components.recorder.const import DATA_INSTANCE +from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index c44c62fe080..c2620aa906d 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -1,9 +1,7 @@ """The tests for the Reddit platform.""" import copy import unittest -from unittest.mock import patch -from homeassistant.components.reddit import sensor as reddit_sensor from homeassistant.components.reddit.sensor import ( ATTR_BODY, ATTR_COMMENTS_NUMBER, @@ -20,7 +18,8 @@ from homeassistant.components.reddit.sensor import ( from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import setup_component -from tests.common import MockDependency, get_test_home_assistant +from tests.async_mock import patch +from tests.common import get_test_home_assistant VALID_CONFIG = { "sensor": { @@ -157,12 +156,10 @@ class TestRedditSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) - def test_setup_with_valid_config(self, mock_praw): + def test_setup_with_valid_config(self): """Test the platform setup with Reddit configuration.""" - with patch.object(reddit_sensor, "praw", mock_praw): - setup_component(self.hass, "sensor", VALID_CONFIG) + setup_component(self.hass, "sensor", VALID_CONFIG) state = self.hass.states.get("sensor.reddit_worldnews") assert int(state.state) == MOCK_RESULTS_LENGTH @@ -184,9 +181,8 @@ class TestRedditSetup(unittest.TestCase): assert state.attributes[CONF_SORT_BY] == "hot" - @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) - def test_setup_with_invalid_config(self, mock_praw): + def test_setup_with_invalid_config(self): """Test the platform setup with invalid Reddit configuration.""" setup_component(self.hass, "sensor", INVALID_SORT_BY_CONFIG) assert not self.hass.states.get("sensor.reddit_worldnews") diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index ba1c24cf6f8..2bba18f0052 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -3,10 +3,10 @@ import json import logging import unittest -from unittest.mock import Mock, mock_open, patch import homeassistant.components.remember_the_milk as rtm +from tests.async_mock import Mock, mock_open, patch from tests.common import get_test_home_assistant _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 24210a38d5c..031131276fe 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -117,3 +117,13 @@ class TestRemote(unittest.TestCase): assert call.domain == remote.DOMAIN assert call.service == SERVICE_LEARN_COMMAND assert call.data[ATTR_ENTITY_ID] == "entity_id_val" + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomRemote(remote.RemoteDevice): + pass + + CustomRemote() + assert "RemoteDevice is deprecated, modify CustomRemote" in caplog.text diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index a4850793ca7..65ae36c3843 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -1,6 +1,5 @@ """The tests for the REST binary sensor platform.""" import unittest -from unittest.mock import Mock, patch import pytest from pytest import raises @@ -15,6 +14,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import template from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index cd2a911292c..c3ed8cea1b9 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the REST sensor platform.""" import unittest -from unittest.mock import Mock, patch import pytest from pytest import raises @@ -16,6 +15,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 788e6d4981f..6a5a0b7f0e2 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -5,7 +5,6 @@ Test setup of rflink sensor component/platform. Verify manual and automatic sensor creation. """ from datetime import timedelta -from unittest.mock import patch from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL from homeassistant.const import ( @@ -17,6 +16,7 @@ from homeassistant.const import ( import homeassistant.core as ha import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 5946adc90a6..fdc60ca7262 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -1,7 +1,5 @@ """Common functions for RFLink component tests and generic platform tests.""" -from unittest.mock import Mock - import pytest from voluptuous.error import MultipleInvalid @@ -17,6 +15,8 @@ from homeassistant.components.rflink import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF +from tests.async_mock import Mock + async def mock_rflink( hass, config, domain, monkeypatch, failures=None, failcommand=False diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 93a6e4f91e0..39b5c339677 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,9 +1,8 @@ """Common methods used across the tests for ring devices.""" -from unittest.mock import patch - from homeassistant.components.ring import DOMAIN from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 0b73c739503..8edaf2c229b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,9 +1,10 @@ """The tests for the Ring binary sensor platform.""" from time import time -from unittest.mock import patch from .common import setup_platform +from tests.async_mock import patch + async def test_binary_sensor(hass, requests_mock): """Test the Ring binary sensors.""" diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 5712106333f..57723d1ede7 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,11 +1,9 @@ """Test the Ring config flow.""" -from unittest.mock import Mock, patch - from homeassistant import config_entries, setup from homeassistant.components.ring import DOMAIN from homeassistant.components.ring.config_flow import InvalidAuth -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_form(hass): @@ -23,9 +21,9 @@ async def test_form(hass): fetch_token=Mock(return_value={"access_token": "mock-token"}) ), ), patch( - "homeassistant.components.ring.async_setup", return_value=mock_coro(True) + "homeassistant.components.ring.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.ring.async_setup_entry", return_value=mock_coro(True), + "homeassistant.components.ring.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index b34ba3d1229..ece4142f8c7 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -1,10 +1,9 @@ """The tests for the rmvtransport platform.""" import datetime -from unittest.mock import patch from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch VALID_CONFIG_MINIMAL = { "sensor": {"platform": "rmvtransport", "next_departure": [{"station": "3000010"}]} @@ -163,8 +162,7 @@ def get_no_departures_mock(): async def test_rmvtransport_min_config(hass): """Test minimal rmvtransport configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", - return_value=mock_coro(get_departures_mock()), + "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) is True @@ -183,8 +181,7 @@ async def test_rmvtransport_min_config(hass): async def test_rmvtransport_name_config(hass): """Test custom name configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", - return_value=mock_coro(get_departures_mock()), + "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_NAME) @@ -195,8 +192,7 @@ async def test_rmvtransport_name_config(hass): async def test_rmvtransport_misc_config(hass): """Test misc configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", - return_value=mock_coro(get_departures_mock()), + "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MISC) @@ -208,8 +204,7 @@ async def test_rmvtransport_misc_config(hass): async def test_rmvtransport_dest_config(hass): """Test destination configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", - return_value=mock_coro(get_departures_mock()), + "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_DEST) @@ -227,7 +222,7 @@ async def test_rmvtransport_no_departures(hass): """Test for no departures.""" with patch( "RMVtransport.RMVtransport.get_departures", - return_value=mock_coro(get_no_departures_mock()), + return_value=get_no_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index 7d6082f2877..e3c4c8ce27c 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -1,11 +1,18 @@ """Tests for the Roku component.""" -from requests_mock import Mocker +import re +from socket import gaierror as SocketGIAError from homeassistant.components.roku.const import DOMAIN +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, +) from homeassistant.const import CONF_HOST from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker HOST = "192.168.1.160" NAME = "Roku 3" @@ -13,38 +20,132 @@ SSDP_LOCATION = "http://192.168.1.160/" UPNP_FRIENDLY_NAME = "My Roku 3" UPNP_SERIAL = "1GU48T017973" +MOCK_SSDP_DISCOVERY_INFO = { + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL: UPNP_SERIAL, +} + def mock_connection( - requests_mocker: Mocker, device: str = "roku3", app: str = "roku", host: str = HOST, + aioclient_mock: AiohttpClientMocker, + device: str = "roku3", + app: str = "roku", + host: str = HOST, + power: bool = True, + error: bool = False, + server_error: bool = False, ) -> None: """Mock the Roku connection.""" roku_url = f"http://{host}:8060" - requests_mocker.get( + if error: + mock_connection_error( + aioclient_mock=aioclient_mock, device=device, app=app, host=host + ) + return + + if server_error: + mock_connection_server_error( + aioclient_mock=aioclient_mock, device=device, app=app, host=host + ) + return + + info_fixture = f"roku/{device}-device-info.xml" + if not power: + info_fixture = f"roku/{device}-device-info-power-off.xml" + + aioclient_mock.get( f"{roku_url}/query/device-info", - text=load_fixture(f"roku/{device}-device-info.xml"), + text=load_fixture(info_fixture), + headers={"Content-Type": "text/xml"}, ) apps_fixture = "roku/apps.xml" if device == "rokutv": apps_fixture = "roku/apps-tv.xml" - requests_mocker.get( - f"{roku_url}/query/apps", text=load_fixture(apps_fixture), + aioclient_mock.get( + f"{roku_url}/query/apps", + text=load_fixture(apps_fixture), + headers={"Content-Type": "text/xml"}, ) - requests_mocker.get( - f"{roku_url}/query/active-app", text=load_fixture(f"roku/active-app-{app}.xml"), + aioclient_mock.get( + f"{roku_url}/query/active-app", + text=load_fixture(f"roku/active-app-{app}.xml"), + headers={"Content-Type": "text/xml"}, ) + aioclient_mock.get( + f"{roku_url}/query/tv-active-channel", + text=load_fixture("roku/rokutv-tv-active-channel.xml"), + headers={"Content-Type": "text/xml"}, + ) + + aioclient_mock.get( + f"{roku_url}/query/tv-channels", + text=load_fixture("roku/rokutv-tv-channels.xml"), + headers={"Content-Type": "text/xml"}, + ) + + aioclient_mock.post( + re.compile(f"{roku_url}/keypress/.*"), text="OK", + ) + + aioclient_mock.post( + re.compile(f"{roku_url}/launch/.*"), text="OK", + ) + + +def mock_connection_error( + aioclient_mock: AiohttpClientMocker, + device: str = "roku3", + app: str = "roku", + host: str = HOST, +) -> None: + """Mock the Roku connection error.""" + roku_url = f"http://{host}:8060" + + aioclient_mock.get(f"{roku_url}/query/device-info", exc=SocketGIAError) + aioclient_mock.get(f"{roku_url}/query/apps", exc=SocketGIAError) + aioclient_mock.get(f"{roku_url}/query/active-app", exc=SocketGIAError) + aioclient_mock.get(f"{roku_url}/query/tv-active-channel", exc=SocketGIAError) + aioclient_mock.get(f"{roku_url}/query/tv-channels", exc=SocketGIAError) + + aioclient_mock.post(re.compile(f"{roku_url}/keypress/.*"), exc=SocketGIAError) + aioclient_mock.post(re.compile(f"{roku_url}/launch/.*"), exc=SocketGIAError) + + +def mock_connection_server_error( + aioclient_mock: AiohttpClientMocker, + device: str = "roku3", + app: str = "roku", + host: str = HOST, +) -> None: + """Mock the Roku server error.""" + roku_url = f"http://{host}:8060" + + aioclient_mock.get(f"{roku_url}/query/device-info", status=500) + aioclient_mock.get(f"{roku_url}/query/apps", status=500) + aioclient_mock.get(f"{roku_url}/query/active-app", status=500) + aioclient_mock.get(f"{roku_url}/query/tv-active-channel", status=500) + aioclient_mock.get(f"{roku_url}/query/tv-channels", status=500) + + aioclient_mock.post(re.compile(f"{roku_url}/keypress/.*"), status=500) + aioclient_mock.post(re.compile(f"{roku_url}/launch/.*"), status=500) + async def setup_integration( hass: HomeAssistantType, - requests_mocker: Mocker, + aioclient_mock: AiohttpClientMocker, device: str = "roku3", app: str = "roku", host: str = HOST, unique_id: str = UPNP_SERIAL, + error: bool = False, + power: bool = True, + server_error: bool = False, skip_entry_setup: bool = False, ) -> MockConfigEntry: """Set up the Roku integration in Home Assistant.""" @@ -53,7 +154,15 @@ async def setup_integration( entry.add_to_hass(hass) if not skip_entry_setup: - mock_connection(requests_mocker, device, app=app, host=host) + mock_connection( + aioclient_mock, + device, + app=app, + host=host, + error=error, + power=power, + server_error=server_error, + ) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index e6c725e9959..403e25e46c6 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,17 +1,5 @@ """Test the Roku config flow.""" -from socket import gaierror as SocketGIAError - -from asynctest import patch -from requests.exceptions import RequestException -from requests_mock import Mocker -from roku import RokuException - from homeassistant.components.roku.const import DOMAIN -from homeassistant.components.ssdp import ( - ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_SERIAL, -) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import ( @@ -22,21 +10,23 @@ from homeassistant.data_entry_flow import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.components.roku import ( HOST, - SSDP_LOCATION, + MOCK_SSDP_DISCOVERY_INFO, UPNP_FRIENDLY_NAME, - UPNP_SERIAL, mock_connection, setup_integration, ) +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_duplicate_error(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_duplicate_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test that errors are shown when duplicates are added.""" - await setup_integration(hass, requests_mock, skip_entry_setup=True) - - mock_connection(requests_mock) + await setup_integration(hass, aioclient_mock, skip_entry_setup=True) + mock_connection(aioclient_mock) user_input = {CONF_HOST: HOST} result = await hass.config_entries.flow.async_init( @@ -54,11 +44,7 @@ async def test_duplicate_error(hass: HomeAssistantType, requests_mock: Mocker) - assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - discovery_info = { - ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_SERIAL: UPNP_SERIAL, - } + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -67,11 +53,12 @@ async def test_duplicate_error(hass: HomeAssistantType, requests_mock: Mocker) - assert result["reason"] == "already_configured" -async def test_form(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_form( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test the user step.""" await async_setup_component(hass, "persistent_notification", {}) - - mock_connection(requests_mock) + mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} @@ -90,7 +77,7 @@ async def test_form(hass: HomeAssistantType, requests_mock: Mocker) -> None: ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST + assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] assert result["data"][CONF_HOST] == HOST @@ -100,70 +87,23 @@ async def test_form(hass: HomeAssistantType, requests_mock: Mocker) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistantType) -> None: +async def test_form_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test we handle cannot connect roku error.""" + mock_connection(aioclient_mock, error=True) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - with patch( - "homeassistant.components.roku.config_flow.Roku._call", - side_effect=RokuException, - ) as mock_validate_input: - result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input={CONF_HOST: HOST} - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 - - -async def test_form_cannot_connect_request(hass: HomeAssistantType) -> None: - """Test we handle cannot connect request error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER} + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) - user_input = {CONF_HOST: HOST} - with patch( - "homeassistant.components.roku.config_flow.Roku._call", - side_effect=RequestException, - ) as mock_validate_input: - result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input=user_input - ) - assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 - - -async def test_form_cannot_connect_socket(hass: HomeAssistantType) -> None: - """Test we handle cannot connect socket error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER} - ) - - user_input = {CONF_HOST: HOST} - with patch( - "homeassistant.components.roku.config_flow.Roku._call", - side_effect=SocketGIAError, - ) as mock_validate_input: - result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input=user_input - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 - async def test_form_unknown_error(hass: HomeAssistantType) -> None: """Test we handle unknown error.""" @@ -173,7 +113,7 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: user_input = {CONF_HOST: HOST} with patch( - "homeassistant.components.roku.config_flow.Roku._call", side_effect=Exception, + "homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception, ) as mock_validate_input: result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=user_input @@ -186,9 +126,11 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: assert len(mock_validate_input.mock_calls) == 1 -async def test_import(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_import( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test the import step.""" - mock_connection(requests_mock) + mock_connection(aioclient_mock) user_input = {CONF_HOST: HOST} with patch( @@ -201,7 +143,7 @@ async def test_import(hass: HomeAssistantType, requests_mock: Mocker) -> None: ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST + assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] assert result["data"][CONF_HOST] == HOST @@ -211,15 +153,44 @@ async def test_import(hass: HomeAssistantType, requests_mock: Mocker) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_ssdp_discovery(hass: HomeAssistantType, requests_mock: Mocker) -> None: - """Test the ssdp discovery step.""" - mock_connection(requests_mock) +async def test_ssdp_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort SSDP flow on connection error.""" + mock_connection(aioclient_mock, error=True) - discovery_info = { - ATTR_SSDP_LOCATION: SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, - ATTR_UPNP_SERIAL: UPNP_SERIAL, - } + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_ssdp_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort SSDP flow on unknown error.""" + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + with patch( + "homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_ssdp_discovery( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the SSDP discovery flow.""" + mock_connection(aioclient_mock) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index c597ebef6f7..3a627db72a5 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -1,11 +1,4 @@ """Tests for the Roku integration.""" -from socket import gaierror as SocketGIAError - -from asynctest import patch -from requests.exceptions import RequestException -from requests_mock import Mocker -from roku import RokuException - from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -14,47 +7,22 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.components.roku import setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker async def test_config_entry_not_ready( - hass: HomeAssistantType, requests_mock: Mocker + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: """Test the Roku configuration entry not ready.""" - with patch( - "homeassistant.components.roku.Roku._call", side_effect=RokuException, - ): - entry = await setup_integration(hass, requests_mock) - - assert entry.state == ENTRY_STATE_SETUP_RETRY - - -async def test_config_entry_not_ready_request( - hass: HomeAssistantType, requests_mock: Mocker -) -> None: - """Test the Roku configuration entry not ready.""" - with patch( - "homeassistant.components.roku.Roku._call", side_effect=RequestException, - ): - entry = await setup_integration(hass, requests_mock) - - assert entry.state == ENTRY_STATE_SETUP_RETRY - - -async def test_config_entry_not_ready_socket( - hass: HomeAssistantType, requests_mock: Mocker -) -> None: - """Test the Roku configuration entry not ready.""" - with patch( - "homeassistant.components.roku.Roku._call", side_effect=SocketGIAError, - ): - entry = await setup_integration(hass, requests_mock) + entry = await setup_integration(hass, aioclient_mock, error=True) assert entry.state == ENTRY_STATE_SETUP_RETRY async def test_unload_config_entry( - hass: HomeAssistantType, requests_mock: Mocker + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: """Test the Roku configuration entry unloading.""" with patch( @@ -63,7 +31,7 @@ async def test_unload_config_entry( ), patch( "homeassistant.components.roku.remote.async_setup_entry", return_value=True, ): - entry = await setup_integration(hass, requests_mock) + entry = await setup_integration(hass, aioclient_mock) assert hass.data[DOMAIN][entry.entry_id] assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 4965207a5b8..f91a8b286b3 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -1,20 +1,19 @@ """Tests for the Roku Media Player platform.""" from datetime import timedelta -from asynctest import PropertyMock, patch -from requests.exceptions import ( - ConnectionError as RequestsConnectionError, - ReadTimeout as RequestsReadTimeout, -) -from requests_mock import Mocker -from roku import RokuException +from rokuecp import RokuError from homeassistant.components.media_player.const import ( + ATTR_APP_ID, + ATTR_APP_NAME, ATTR_INPUT_SOURCE, + ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, + MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, @@ -26,7 +25,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -39,6 +38,7 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, STATE_HOME, + STATE_IDLE, STATE_PLAYING, STATE_STANDBY, STATE_UNAVAILABLE, @@ -46,8 +46,10 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.roku import UPNP_SERIAL, setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker MAIN_ENTITY_ID = f"{MP_DOMAIN}.my_roku_3" TV_ENTITY_ID = f"{MP_DOMAIN}.58_onn_roku_tv" @@ -56,34 +58,37 @@ TV_HOST = "192.168.1.161" TV_SERIAL = "YN00H5555555" -async def test_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with basic config.""" - await setup_integration(hass, requests_mock) + await setup_integration(hass, aioclient_mock) entity_registry = await hass.helpers.entity_registry.async_get_registry() - main = entity_registry.async_get(MAIN_ENTITY_ID) + assert hass.states.get(MAIN_ENTITY_ID) + assert main assert main.unique_id == UPNP_SERIAL -async def test_idle_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_idle_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with idle device.""" - with patch( - "homeassistant.components.roku.Roku.power_state", - new_callable=PropertyMock(return_value="Off"), - ): - await setup_integration(hass, requests_mock) + await setup_integration(hass, aioclient_mock, power=False) state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_STANDBY -async def test_tv_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_tv_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test Roku TV setup.""" await setup_integration( hass, - requests_mock, + aioclient_mock, device="rokutv", app="tvinput-dtv", host=TV_HOST, @@ -91,41 +96,26 @@ async def test_tv_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None: ) entity_registry = await hass.helpers.entity_registry.async_get_registry() - tv = entity_registry.async_get(TV_ENTITY_ID) + assert hass.states.get(TV_ENTITY_ID) + assert tv assert tv.unique_id == TV_SERIAL -async def test_availability(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_availability( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test entity availability.""" now = dt_util.utcnow() future = now + timedelta(minutes=1) with patch("homeassistant.util.dt.utcnow", return_value=now): - await setup_integration(hass, requests_mock) + await setup_integration(hass, aioclient_mock) - with patch("roku.Roku._get", side_effect=RokuException,), patch( - "homeassistant.util.dt.utcnow", return_value=future - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE - - future += timedelta(minutes=1) - - with patch("roku.Roku._get", side_effect=RequestsConnectionError,), patch( - "homeassistant.util.dt.utcnow", return_value=future - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE - - future += timedelta(minutes=1) - - with patch("roku.Roku._get", side_effect=RequestsReadTimeout,), patch( - "homeassistant.util.dt.utcnow", return_value=future - ): + with patch( + "homeassistant.components.roku.Roku.update", side_effect=RokuError + ), patch("homeassistant.util.dt.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE @@ -139,17 +129,17 @@ async def test_availability(hass: HomeAssistantType, requests_mock: Mocker) -> N async def test_supported_features( - hass: HomeAssistantType, requests_mock: Mocker + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: """Test supported features.""" - await setup_integration(hass, requests_mock) + await setup_integration(hass, aioclient_mock) # Features supported for Rokus state = hass.states.get(MAIN_ENTITY_ID) assert ( SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY @@ -161,12 +151,12 @@ async def test_supported_features( async def test_tv_supported_features( - hass: HomeAssistantType, requests_mock: Mocker + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: """Test supported features for Roku TV.""" await setup_integration( hass, - requests_mock, + aioclient_mock, device="rokutv", app="tvinput-dtv", host=TV_HOST, @@ -177,7 +167,7 @@ async def test_tv_supported_features( assert ( SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY @@ -188,22 +178,58 @@ async def test_tv_supported_features( ) -async def test_attributes(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_attributes( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test attributes.""" - await setup_integration(hass, requests_mock) + await setup_integration(hass, aioclient_mock) state = hass.states.get(MAIN_ENTITY_ID) assert state.state == STATE_HOME assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) is None + assert state.attributes.get(ATTR_APP_ID) is None + assert state.attributes.get(ATTR_APP_NAME) == "Roku" assert state.attributes.get(ATTR_INPUT_SOURCE) == "Roku" -async def test_tv_attributes(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_attributes_app( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test attributes for app.""" + await setup_integration(hass, aioclient_mock, app="netflix") + + state = hass.states.get(MAIN_ENTITY_ID) + assert state.state == STATE_PLAYING + + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert state.attributes.get(ATTR_APP_ID) == "12" + assert state.attributes.get(ATTR_APP_NAME) == "Netflix" + assert state.attributes.get(ATTR_INPUT_SOURCE) == "Netflix" + + +async def test_attributes_screensaver( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test attributes for app with screensaver.""" + await setup_integration(hass, aioclient_mock, app="screensaver") + + state = hass.states.get(MAIN_ENTITY_ID) + assert state.state == STATE_IDLE + + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) is None + assert state.attributes.get(ATTR_APP_ID) is None + assert state.attributes.get(ATTR_APP_NAME) == "Roku" + assert state.attributes.get(ATTR_INPUT_SOURCE) == "Roku" + + +async def test_tv_attributes( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test attributes for Roku TV.""" await setup_integration( hass, - requests_mock, + aioclient_mock, device="rokutv", app="tvinput-dtv", host=TV_HOST, @@ -213,29 +239,35 @@ async def test_tv_attributes(hass: HomeAssistantType, requests_mock: Mocker) -> state = hass.states.get(TV_ENTITY_ID) assert state.state == STATE_PLAYING - assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_CHANNEL + assert state.attributes.get(ATTR_APP_ID) == "tvinput.dtv" + assert state.attributes.get(ATTR_APP_NAME) == "Antenna TV" assert state.attributes.get(ATTR_INPUT_SOURCE) == "Antenna TV" + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_CHANNEL + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "getTV (14.3)" + assert state.attributes.get(ATTR_MEDIA_TITLE) == "Airwolf" -async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_services( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test the different media player services.""" - await setup_integration(hass, requests_mock) + await setup_integration(hass, aioclient_mock) - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, blocking=True ) - remote_mock.assert_called_once_with("/keypress/PowerOff") + remote_mock.assert_called_once_with("poweroff") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, blocking=True ) - remote_mock.assert_called_once_with("/keypress/PowerOn") + remote_mock.assert_called_once_with("poweron") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, @@ -243,9 +275,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: blocking=True, ) - remote_mock.assert_called_once_with("/keypress/Play") + remote_mock.assert_called_once_with("play") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, @@ -253,9 +285,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: blocking=True, ) - remote_mock.assert_called_once_with("/keypress/Fwd") + remote_mock.assert_called_once_with("forward") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, @@ -263,9 +295,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: blocking=True, ) - remote_mock.assert_called_once_with("/keypress/Rev") + remote_mock.assert_called_once_with("reverse") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -273,9 +305,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: blocking=True, ) - remote_mock.assert_called_once_with("/keypress/Home") + remote_mock.assert_called_once_with("home") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.launch") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -283,28 +315,30 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: blocking=True, ) - remote_mock.assert_called_once_with("/launch/12", params={"contentID": "12"}) + remote_mock.assert_called_once_with("12") -async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> None: +async def test_tv_services( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test the media player services related to Roku TV.""" await setup_integration( hass, - requests_mock, + aioclient_mock, device="rokutv", app="tvinput-dtv", host=TV_HOST, unique_id=TV_SERIAL, ) - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: TV_ENTITY_ID}, blocking=True ) - remote_mock.assert_called_once_with("/keypress/VolumeUp") + remote_mock.assert_called_once_with("volume_up") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_DOWN, @@ -312,9 +346,9 @@ async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> No blocking=True, ) - remote_mock.assert_called_once_with("/keypress/VolumeDown") + remote_mock.assert_called_once_with("volume_down") - with patch("roku.Roku._post") as remote_mock: + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_MUTE, @@ -322,9 +356,9 @@ async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> No blocking=True, ) - remote_mock.assert_called_once_with("/keypress/VolumeMute") + remote_mock.assert_called_once_with("volume_mute") - with patch("roku.Roku.launch") as tune_mock: + with patch("homeassistant.components.roku.Roku.tune") as tune_mock: await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -336,4 +370,4 @@ async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> No blocking=True, ) - tune_mock.assert_called_once() + tune_mock.assert_called_once_with("55") diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py new file mode 100644 index 00000000000..6b50d4362c1 --- /dev/null +++ b/tests/components/roku/test_remote.py @@ -0,0 +1,70 @@ +"""The tests for the Roku remote platform.""" +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.helpers.typing import HomeAssistantType + +from tests.async_mock import patch +from tests.components.roku import UPNP_SERIAL, setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker + +MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.my_roku_3" + +# pylint: disable=redefined-outer-name + + +async def test_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with basic config.""" + await setup_integration(hass, aioclient_mock) + assert hass.states.get(MAIN_ENTITY_ID) + + +async def test_unique_id( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test unique id.""" + await setup_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + main = entity_registry.async_get(MAIN_ENTITY_ID) + assert main.unique_id == UPNP_SERIAL + + +async def test_main_services( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the different services.""" + await setup_integration(hass, aioclient_mock) + + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, + blocking=True, + ) + remote_mock.assert_called_once_with("poweroff") + + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, + blocking=True, + ) + remote_mock.assert_called_once_with("poweron") + + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_COMMAND: ["home"]}, + blocking=True, + ) + remote_mock.assert_called_once_with("home") diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 529b59dd8a6..197ad56f415 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1,5 +1,4 @@ """Test the iRobot Roomba config flow.""" -from asynctest import MagicMock, PropertyMock, patch from roomba import RoombaConnectionError from homeassistant import config_entries, data_entry_flow, setup @@ -11,6 +10,7 @@ from homeassistant.components.roomba.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4e5a02588b1..2673ee56559 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,7 +1,4 @@ """Tests for Samsung TV config flow.""" -from unittest.mock import Mock, PropertyMock, call, patch - -from asynctest import mock import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.exceptions import ConnectionFailure @@ -21,6 +18,8 @@ from homeassistant.components.ssdp import ( ) from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN +from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch + MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", @@ -70,11 +69,11 @@ def remote_fixture(): ) as remote_class, patch( "homeassistant.components.samsungtv.config_flow.socket" ) as socket_class: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote - socket = mock.Mock() + socket = Mock() socket_class.return_value = socket socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remote @@ -88,12 +87,12 @@ def remotews_fixture(): ) as remotews_class, patch( "homeassistant.components.samsungtv.config_flow.socket" ) as socket_class: - remotews = mock.Mock() - remotews.__enter__ = mock.Mock() - remotews.__exit__ = mock.Mock() + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() remotews_class.return_value = remotews remotews_class().__enter__().token = "FAKE_TOKEN" - socket = mock.Mock() + socket = Mock() socket_class.return_value = socket socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remotews @@ -486,7 +485,7 @@ async def test_autodetect_websocket_ssl(hass, remote, remotews): "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=[WebSocketProtocolException("Boom"), mock.DEFAULT], + side_effect=[WebSocketProtocolException("Boom"), DEFAULT_MOCK], ) as remotews: enter = Mock() type(enter).token = PropertyMock(return_value="123456789") diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 232a04416d5..5ef47cb3106 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,6 +1,4 @@ """Tests for the Samsung TV Integration.""" -from asynctest import mock -from asynctest.mock import call, patch import pytest from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON @@ -18,6 +16,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, call, patch + ENTITY_ID = f"{DOMAIN}.fake_name" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ @@ -49,9 +49,9 @@ def remote_fixture(): ) as socket1, patch( "homeassistant.components.samsungtv.socket" ) as socket2: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c549e57a06e..15ac13c64d5 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -3,8 +3,6 @@ import asyncio from datetime import timedelta import logging -from asynctest import mock -from asynctest.mock import call, patch import pytest from samsungctl import exceptions from samsungtvws.exceptions import ConnectionFailure @@ -54,6 +52,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" @@ -120,9 +119,9 @@ def remote_fixture(): ) as socket1, patch( "homeassistant.components.samsungtv.socket" ) as socket2: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" @@ -139,9 +138,9 @@ def remotews_fixture(): ) as socket1, patch( "homeassistant.components.samsungtv.socket" ) as socket2: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote remote_class().__enter__().token = "FAKE_TOKEN" socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" @@ -185,11 +184,11 @@ async def test_setup_without_turnon(hass, remote): async def test_setup_websocket(hass, remotews, mock_now): """Test setup of platform.""" with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: - enter = mock.Mock() - type(enter).token = mock.PropertyMock(return_value="987654321") - remote = mock.Mock() - remote.__enter__ = mock.Mock(return_value=enter) - remote.__exit__ = mock.Mock() + enter = Mock() + type(enter).token = PropertyMock(return_value="987654321") + remote = Mock() + remote.__enter__ = Mock(return_value=enter) + remote.__exit__ = Mock() remote_class.return_value = remote await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -247,7 +246,7 @@ async def test_update_off(hass, remote, mock_now): with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), mock.DEFAULT], + side_effect=[OSError("Boom"), DEFAULT_MOCK], ): next_update = mock_now + timedelta(minutes=5) @@ -283,7 +282,7 @@ async def test_update_connection_failure(hass, remotews, mock_now): """Testing update tv connection failure exception.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), mock.DEFAULT], + side_effect=[OSError("Boom"), DEFAULT_MOCK], ): await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -309,7 +308,7 @@ async def test_update_unhandled_response(hass, remote, mock_now): with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[exceptions.UnhandledResponse("Boom"), mock.DEFAULT], + side_effect=[exceptions.UnhandledResponse("Boom"), DEFAULT_MOCK], ): next_update = mock_now + timedelta(minutes=5) @@ -339,7 +338,7 @@ async def test_send_key(hass, remote): async def test_send_key_broken_pipe(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=BrokenPipeError("Boom")) + remote.control = Mock(side_effect=BrokenPipeError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -350,8 +349,8 @@ async def test_send_key_broken_pipe(hass, remote): async def test_send_key_connection_closed_retry_succeed(hass, remote): """Test retry on connection closed.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock( - side_effect=[exceptions.ConnectionClosed("Boom"), mock.DEFAULT, mock.DEFAULT] + remote.control = Mock( + side_effect=[exceptions.ConnectionClosed("Boom"), DEFAULT_MOCK, DEFAULT_MOCK] ) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True @@ -371,7 +370,7 @@ async def test_send_key_connection_closed_retry_succeed(hass, remote): async def test_send_key_unhandled_response(hass, remote): """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=exceptions.UnhandledResponse("Boom")) + remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -382,7 +381,7 @@ async def test_send_key_unhandled_response(hass, remote): async def test_send_key_websocketexception(hass, remote): """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=WebSocketException("Boom")) + remote.control = Mock(side_effect=WebSocketException("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -393,7 +392,7 @@ async def test_send_key_websocketexception(hass, remote): async def test_send_key_os_error(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=OSError("Boom")) + remote.control = Mock(side_effect=OSError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -467,7 +466,7 @@ async def test_turn_off_websocket(hass, remotews): """Test for turn_off.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), mock.DEFAULT], + side_effect=[OSError("Boom"), DEFAULT_MOCK], ): await setup_samsungtv(hass, MOCK_CONFIGWS) assert await hass.services.async_call( @@ -493,7 +492,7 @@ async def test_turn_off_os_error(hass, remote, caplog): """Test for turn_off with OSError.""" caplog.set_level(logging.DEBUG) await setup_samsungtv(hass, MOCK_CONFIG) - remote.close = mock.Mock(side_effect=OSError("BOOM")) + remote.close = Mock(side_effect=OSError("BOOM")) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index e4a4d4ca239..8dbe43a25ff 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,7 +1,6 @@ """The tests for the Script component.""" # pylint: disable=protected-access import unittest -from unittest.mock import Mock, patch import pytest @@ -22,6 +21,7 @@ from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant ENTITY_ID = "script.test" diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index fdce335b7cf..5a38090b5c9 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Sense config flow.""" -from asynctest import patch from sense_energy import SenseAPITimeoutException, SenseAuthenticationException from homeassistant import config_entries, setup from homeassistant.components.sense.const import DOMAIN +from tests.async_mock import patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 7ce34c13f53..25353751f91 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -1,12 +1,10 @@ """Test the sentry config flow.""" -from unittest.mock import patch - from sentry_sdk.utils import BadDsn from homeassistant import config_entries, setup from homeassistant.components.sentry.const import DOMAIN -from tests.common import mock_coro +from tests.async_mock import patch async def test_form(hass): @@ -20,12 +18,11 @@ async def test_form(hass): with patch( "homeassistant.components.sentry.config_flow.validate_input", - return_value=mock_coro({"title": "Sentry"}), + return_value={"title": "Sentry"}, ), patch( - "homeassistant.components.sentry.async_setup", return_value=mock_coro(True) + "homeassistant.components.sentry.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.sentry.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.sentry.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"dsn": "http://public@sentry.local/1"}, diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 10ec22f8b67..62f93d7cd70 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.common import MockDependency, async_fire_time_changed +from tests.common import async_fire_time_changed VALID_CONFIG_MINIMAL = { "sensor": { @@ -110,15 +110,8 @@ class ProfileMock: return self.__class__.summary_data -@pytest.fixture(autouse=True, name="mock_py17track") -def fixture_mock_py17track(): - """Mock py17track dependency.""" - with MockDependency("py17track"): - yield - - @pytest.fixture(autouse=True, name="mock_client") -def fixture_mock_client(mock_py17track): +def fixture_mock_client(): """Mock py17track client.""" with mock.patch( "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index a5e8cb1d946..8743ab27bd7 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -4,15 +4,14 @@ import os import tempfile from typing import Tuple import unittest -from unittest.mock import Mock, patch from homeassistant.components import shell_command from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant -@asyncio.coroutine def mock_process_creator(error: bool = False) -> asyncio.coroutine: """Mock a coroutine that creates a process when yielded.""" diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index f63363e2f63..8307a6845b1 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -1,9 +1,9 @@ """Shopping list test helpers.""" -from asynctest import patch import pytest from homeassistant.components.shopping_list import intent as sl_intent +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index dbfd19795e8..a44be249f22 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -3,7 +3,6 @@ import os import tempfile import unittest -from unittest.mock import patch from pysignalclirestapi import SignalCliRestApi import requests_mock @@ -11,6 +10,8 @@ import requests_mock import homeassistant.components.signal_messenger.notify as signalmessenger from homeassistant.setup import async_setup_component +from tests.async_mock import patch + BASE_COMPONENT = "notify" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index f53636fc440..2448b20b084 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,8 +1,6 @@ """Define tests for the SimpliSafe config flow.""" import json -from unittest.mock import MagicMock, PropertyMock, mock_open -from asynctest import patch from simplipy.errors import SimplipyError from homeassistant import data_entry_flow @@ -10,6 +8,7 @@ from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from tests.async_mock import MagicMock, PropertyMock, mock_open, patch from tests.common import MockConfigEntry diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index b8c3a2cd2e8..fbafe8aad7d 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -1,12 +1,12 @@ """The tests for SleepIQ binary sensor platform.""" import unittest -from unittest.mock import MagicMock import requests_mock from homeassistant.components.sleepiq import binary_sensor as sleepiq from homeassistant.setup import setup_component +from tests.async_mock import MagicMock from tests.common import get_test_home_assistant from tests.components.sleepiq.test_init import mock_responses diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 6626be41a6b..9c1c0972fac 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,12 +1,12 @@ """The tests for the SleepIQ component.""" import unittest -from unittest.mock import MagicMock, patch import requests_mock from homeassistant import setup import homeassistant.components.sleepiq as sleepiq +from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, load_fixture diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index a049dfd2fbf..d94cd7e4063 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -1,12 +1,12 @@ """The tests for SleepIQ sensor platform.""" import unittest -from unittest.mock import MagicMock import requests_mock import homeassistant.components.sleepiq.sensor as sleepiq from homeassistant.setup import setup_component +from tests.async_mock import MagicMock from tests.common import get_test_home_assistant from tests.components.sleepiq.test_init import mock_responses diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index e05917c17d8..83f8e4dfca1 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -2,7 +2,6 @@ import secrets from uuid import uuid4 -from asynctest import Mock, patch from pysmartthings import ( CLASSIFICATION_AUTOMATION, AppEntity, @@ -38,10 +37,12 @@ from homeassistant.components.smartthings.const import ( STORAGE_KEY, STORAGE_VERSION, ) +from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry COMPONENT_PREFIX = "homeassistant.components.smartthings." @@ -73,8 +74,10 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): async def setup_component(hass, config_file, hass_storage): """Load the SmartThing component.""" hass_storage[STORAGE_KEY] = {"data": config_file, "version": STORAGE_VERSION} + await async_process_ha_core_config( + hass, {"external_url": "https://test.local"}, + ) await async_setup_component(hass, "smartthings", {}) - hass.config.api.base_url = "https://test.local" def _create_location(): @@ -97,7 +100,7 @@ def locations_fixture(location): @pytest.fixture(name="app") -def app_fixture(hass, config_file): +async def app_fixture(hass, config_file): """Fixture for a single app.""" app = Mock(AppEntity) app.app_name = APP_NAME_PREFIX + str(uuid4()) @@ -105,7 +108,7 @@ def app_fixture(hass, config_file): app.app_type = "WEBHOOK_SMART_APP" app.classifications = [CLASSIFICATION_AUTOMATION] app.display_name = "Home Assistant" - app.description = f"{hass.config.location_name} at {hass.config.api.base_url}" + app.description = f"{hass.config.location_name} at https://test.local" app.single_instance = True app.webhook_target_url = webhook.async_generate_url( hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 81dbab917a3..47726bfe270 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -2,7 +2,6 @@ from uuid import uuid4 from aiohttp import ClientResponseError -from asynctest import Mock, patch from pysmartthings import APIResponseError from pysmartthings.installedapp import format_install_url @@ -16,6 +15,7 @@ from homeassistant.components.smartthings.const import ( CONF_OAUTH_CLIENT_SECRET, DOMAIN, ) +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( CONF_ACCESS_TOKEN, HTTP_FORBIDDEN, @@ -23,7 +23,8 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import MockConfigEntry async def test_import_shows_user_step(hass): @@ -342,8 +343,8 @@ async def test_entry_created_with_cloudhook( installed_app_id = str(uuid4()) refresh_token = str(uuid4()) smartthings_mock.apps.return_value = [] - smartthings_mock.create_app.return_value = (app, app_oauth_client) - smartthings_mock.locations.return_value = [location] + smartthings_mock.create_app = AsyncMock(return_value=(app, app_oauth_client)) + smartthings_mock.locations = AsyncMock(return_value=[location]) request = Mock() request.installed_app_id = installed_app_id request.auth_token = token @@ -351,11 +352,11 @@ async def test_entry_created_with_cloudhook( request.refresh_token = refresh_token with patch.object( - hass.components.cloud, "async_active_subscription", return_value=True + hass.components.cloud, "async_active_subscription", Mock(return_value=True) ), patch.object( hass.components.cloud, "async_create_cloudhook", - return_value=mock_coro("http://cloud.test"), + AsyncMock(return_value="http://cloud.test"), ) as mock_create_cloudhook: await smartapp.setup_smartapp_endpoint(hass) @@ -417,9 +418,10 @@ async def test_entry_created_with_cloudhook( async def test_invalid_webhook_aborts(hass): """Test flow aborts if webhook is invalid.""" - hass.config.api.base_url = "http://0.0.0.0" - # Webhook confirmation shown + await async_process_ha_core_config( + hass, {"external_url": "http://example.local:8123"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 78142ae3fc4..014b9a6673c 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -2,7 +2,6 @@ from uuid import uuid4 from aiohttp import ClientConnectionError, ClientResponseError -from asynctest import Mock, patch from pysmartthings import InstalledAppStatus, OAuthToken import pytest @@ -17,11 +16,13 @@ from homeassistant.components.smartthings.const import ( SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS, ) +from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry @@ -116,7 +117,9 @@ async def test_base_url_no_longer_https_does_not_load( hass, config_entry, app, smartthings_mock ): """Test base_url no longer valid creates a new flow.""" - hass.config.api.base_url = "http://0.0.0.0" + await async_process_ha_core_config( + hass, {"external_url": "http://example.local:8123"}, + ) config_entry.add_to_hass(hass) smartthings_mock.app.return_value = app @@ -218,7 +221,6 @@ async def test_config_entry_loads_unconnected_cloud( """Test entry loads during startup when cloud isn't connected.""" config_entry.add_to_hass(hass) hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud" - hass.config.api.base_url = "http://0.0.0.0" smartthings_mock.app.return_value = app smartthings_mock.installed_app.return_value = installed_app smartthings_mock.devices.return_value = [device] diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index efc4844cef2..458e5f8ce27 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -1,7 +1,6 @@ """Tests for the smartapp module.""" from uuid import uuid4 -from asynctest import CoroutineMock, Mock, patch from pysmartthings import AppEntity, Capability from homeassistant.components.smartthings import smartapp @@ -11,6 +10,7 @@ from homeassistant.components.smartthings.const import ( DOMAIN, ) +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -76,11 +76,11 @@ async def test_smartapp_uninstall(hass, config_entry): async def test_smartapp_webhook(hass): """Test the smartapp webhook calls the manager.""" manager = Mock() - manager.handle_request = CoroutineMock(return_value={}) + manager.handle_request = AsyncMock(return_value={}) hass.data[DOMAIN][DATA_MANAGER] = manager request = Mock() request.headers = [] - request.json = CoroutineMock(return_value={}) + request.json = AsyncMock(return_value={}) result = await smartapp.smartapp_webhook(hass, "", request) assert result.body == b"{}" diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 6f215840324..92c9e13fb8a 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -1,5 +1,5 @@ """Common test utilities.""" -from unittest.mock import Mock +from tests.async_mock import Mock class AsyncMock(Mock): diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index b983f8af487..56a0745c1b3 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -1,12 +1,10 @@ """Tests for SMHI config flow.""" -from unittest.mock import Mock, patch - from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException from homeassistant.components.smhi import config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.common import mock_coro +from tests.async_mock import Mock, patch # pylint: disable=protected-access @@ -15,7 +13,7 @@ async def test_homeassistant_location_exists() -> None: hass = Mock() flow = config_flow.SmhiFlowHandler() flow.hass = hass - with patch.object(flow, "_check_location", return_value=mock_coro(True)): + with patch.object(flow, "_check_location", return_value=True): # Test exists hass.config.location_name = "Home" hass.config.latitude = 17.8419 @@ -100,7 +98,7 @@ async def test_flow_with_home_location(hass) -> None: flow = config_flow.SmhiFlowHandler() flow.hass = hass - with patch.object(flow, "_check_location", return_value=mock_coro(True)): + with patch.object(flow, "_check_location", return_value=True): hass.config.location_name = "Home" hass.config.latitude = 17.8419 hass.config.longitude = 59.3262 @@ -122,9 +120,9 @@ async def test_flow_show_form() -> None: # Test show form when Home Assistant config exists and # home is already configured, then new config is allowed with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(True) + flow, "_homeassistant_location_exists", return_value=True ), patch.object( config_flow, "smhi_locations", @@ -136,9 +134,9 @@ async def test_flow_show_form() -> None: # Test show form when Home Assistant config not and # home is not configured with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(False) + flow, "_homeassistant_location_exists", return_value=False ), patch.object( config_flow, "smhi_locations", @@ -161,7 +159,7 @@ async def test_flow_show_form_name_exists() -> None: # Test show form when Home Assistant config exists and # home is already configured, then new config is allowed with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( flow, "_name_in_configuration_exists", return_value=True ), patch.object( @@ -169,7 +167,7 @@ async def test_flow_show_form_name_exists() -> None: "smhi_locations", return_value={"test": "something", "name_exist": "config"}, ), patch.object( - flow, "_check_location", return_value=mock_coro(True) + flow, "_check_location", return_value=True ): await flow.async_step_user(user_input=test_data) @@ -191,17 +189,17 @@ async def test_flow_entry_created_from_user_input() -> None: # Test that entry created when user_input name not exists with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( flow, "_name_in_configuration_exists", return_value=False ), patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(False) + flow, "_homeassistant_location_exists", return_value=False ), patch.object( config_flow, "smhi_locations", return_value={"test": "something", "name_exist": "config"}, ), patch.object( - flow, "_check_location", return_value=mock_coro(True) + flow, "_check_location", return_value=True ): result = await flow.async_step_user(user_input=test_data) @@ -224,20 +222,18 @@ async def test_flow_entry_created_user_input_faulty() -> None: test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} # Test that entry created when user_input name not exists - with patch.object( - flow, "_check_location", return_value=mock_coro(True) - ), patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch.object(flow, "_check_location", return_value=True), patch.object( + flow, "_show_config_form", return_value=None ) as config_form, patch.object( flow, "_name_in_configuration_exists", return_value=False ), patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(False) + flow, "_homeassistant_location_exists", return_value=False ), patch.object( config_flow, "smhi_locations", return_value={"test": "something", "name_exist": "config"}, ), patch.object( - flow, "_check_location", return_value=mock_coro(False) + flow, "_check_location", return_value=False ): await flow.async_step_user(user_input=test_data) @@ -254,7 +250,7 @@ async def test_check_location_correct() -> None: with patch.object( config_flow.aiohttp_client, "async_get_clientsession" - ), patch.object(SmhiApi, "async_get_forecast", return_value=mock_coro()): + ), patch.object(SmhiApi, "async_get_forecast", return_value=None): assert await flow._check_location("58", "17") is True diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index 450ac7e6ef0..e6b523d96bb 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,10 +1,10 @@ """Test SMHI component setup process.""" -from unittest.mock import Mock - from homeassistant.components import smhi from .common import AsyncMock +from tests.async_mock import Mock + TEST_CONFIG = { "config": { "name": "0123456789ABCDEF", diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 3485a108d5b..6f5fdfd2333 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -2,7 +2,8 @@ import asyncio from datetime import datetime import logging -from unittest.mock import Mock, patch + +from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException from homeassistant.components.smhi import weather as weather_smhi from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS @@ -24,6 +25,7 @@ from homeassistant.components.weather import ( from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry, load_fixture _LOGGER = logging.getLogger(__name__) @@ -39,8 +41,6 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: "async_forward_entry_setup". The actual result are tested with the entity state rather than "per function" unity tests """ - from smhi.smhi_lib import APIURL_TEMPLATE - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) api_response = load_fixture("smhi.json") aioclient_mock.get(uri, text=api_response) @@ -149,7 +149,6 @@ def test_properties_unknown_symbol() -> None: # pylint: disable=protected-access async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: """Test the refresh weather forecast function.""" - from smhi.smhi_lib import SmhiForecastException with patch.object( hass.helpers.event, "async_call_later" @@ -191,7 +190,6 @@ async def test_refresh_weather_forecast_timeout(hass) -> None: async def test_refresh_weather_forecast_exception() -> None: """Test any exception.""" - from smhi.smhi_lib import SmhiForecastException hass = Mock() weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") @@ -199,17 +197,9 @@ async def test_refresh_weather_forecast_exception() -> None: with patch.object( hass.helpers.event, "async_call_later" - ) as call_later, patch.object(weather_smhi, "async_timeout"), patch.object( - weather_smhi.SmhiWeather, "retry_update" - ), patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=SmhiForecastException(), + ) as call_later, patch.object( + weather, "get_weather_forecast", side_effect=SmhiForecastException(), ): - - hass.async_add_job = Mock() - call_later = hass.helpers.event.async_call_later - await weather.async_update() assert len(call_later.mock_calls) == 1 # Assert we are going to wait RETRY_TIMEOUT seconds @@ -222,8 +212,8 @@ async def test_retry_update(): weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") weather.hass = hass - with patch.object(weather_smhi.SmhiWeather, "async_update") as update: - await weather.retry_update() + with patch.object(weather, "async_update", AsyncMock()) as update: + await weather.retry_update(None) assert len(update.mock_calls) == 1 diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 6c7e41a4728..f74d47a21c1 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,10 +1,10 @@ """The tests for the notify smtp platform.""" import re import unittest -from unittest.mock import patch from homeassistant.components.smtp.notify import MailNotificationService +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 759639362e4..61bc5f9ac6c 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the SolarEdge config flow.""" -from unittest.mock import Mock, patch - import pytest from requests.exceptions import ConnectTimeout, HTTPError @@ -9,6 +7,7 @@ from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME from homeassistant.const import CONF_API_KEY, CONF_NAME +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry NAME = "solaredge site 1 2 3" diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 7828290560a..1474b8e13e7 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,6 +1,4 @@ """Test the solarlog config flow.""" -from unittest.mock import patch - import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -8,7 +6,8 @@ from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry NAME = "Solarlog test 1 2 3" HOST = "http://1.1.1.1" @@ -25,12 +24,11 @@ async def test_form(hass): with patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value=mock_coro({"title": "solarlog test 1 2 3"}), + return_value={"title": "solarlog test 1 2 3"}, ), patch( - "homeassistant.components.solarlog.async_setup", return_value=mock_coro(True) + "homeassistant.components.solarlog.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.solarlog.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.solarlog.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": HOST, "name": NAME} @@ -49,7 +47,7 @@ def mock_controller(): """Mock a successful _host_in_configuration_exists.""" with patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - side_effect=lambda *_: mock_coro(True), + return_value=True, ): yield diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 1d00f83a608..929463ecf81 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for the Soma config flow.""" -from unittest.mock import patch - from api.soma_api import SomaApi from requests import RequestException from homeassistant import data_entry_flow from homeassistant.components.soma import DOMAIN, config_flow +from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_HOST = "123.45.67.89" diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index f195b640240..1823cb3c3ab 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the Somfy config flow.""" import asyncio -from unittest.mock import patch import pytest @@ -8,6 +7,7 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.somfy import DOMAIN, config_flow from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_SECRET_VALUE = "5678" diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 1629a3d29c2..96585f87068 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -8,6 +8,7 @@ import pytest import homeassistant.components.sonarr.sensor as sonarr from homeassistant.const import DATA_GIGABYTES, UNIT_PERCENTAGE +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -491,7 +492,7 @@ class TestSonarrSetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_diskspace_no_paths(self, req_mock): """Test getting all disk space.""" config = { @@ -511,7 +512,7 @@ class TestSonarrSetup(unittest.TestCase): assert "Sonarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_diskspace_paths(self, req_mock): """Test getting diskspace for included paths.""" config = { @@ -531,7 +532,7 @@ class TestSonarrSetup(unittest.TestCase): assert "Sonarr Disk Space" == device.name assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_commands(self, req_mock): """Test getting running commands.""" config = { @@ -551,7 +552,7 @@ class TestSonarrSetup(unittest.TestCase): assert "Sonarr Commands" == device.name assert "pending" == device.device_state_attributes["RescanSeries"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_queue(self, req_mock): """Test getting downloads in the queue.""" config = { @@ -574,7 +575,7 @@ class TestSonarrSetup(unittest.TestCase): == device.device_state_attributes["Game of Thrones S03E08"] ) - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_series(self, req_mock): """Test getting the number of series.""" config = { @@ -596,7 +597,7 @@ class TestSonarrSetup(unittest.TestCase): "26/26 Episodes" == device.device_state_attributes["Marvel's Daredevil"] ) - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_wanted(self, req_mock): """Test getting wanted episodes.""" config = { @@ -618,7 +619,7 @@ class TestSonarrSetup(unittest.TestCase): "2014-02-03" == device.device_state_attributes["Archer (2009) S05E04"] ) - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_upcoming_multiple_days(self, req_mock): """Test the upcoming episodes for multiple days.""" config = { @@ -639,7 +640,7 @@ class TestSonarrSetup(unittest.TestCase): assert "S04E11" == device.device_state_attributes["Bob's Burgers"] @pytest.mark.skip - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_upcoming_today(self, req_mock): """Test filtering for a single day. @@ -662,7 +663,7 @@ class TestSonarrSetup(unittest.TestCase): assert "Sonarr Upcoming" == device.name assert "S04E11" == device.device_state_attributes["Bob's Burgers"] - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_system_status(self, req_mock): """Test getting system status.""" config = { @@ -682,7 +683,7 @@ class TestSonarrSetup(unittest.TestCase): assert "6.2.9200.0" == device.device_state_attributes["osVersion"] @pytest.mark.skip - @unittest.mock.patch("requests.get", side_effect=mocked_requests_get) + @patch("requests.get", side_effect=mocked_requests_get) def test_ssl(self, req_mock): """Test SSL being enabled.""" config = { @@ -704,7 +705,7 @@ class TestSonarrSetup(unittest.TestCase): assert "Sonarr Upcoming" == device.name assert "S04E11" == device.device_state_attributes["Bob's Burgers"] - @unittest.mock.patch("requests.get", side_effect=mocked_exception) + @patch("requests.get", side_effect=mocked_exception) def test_exception_handling(self, req_mock): """Test exception being handled.""" config = { diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py new file mode 100644 index 00000000000..bca879268cc --- /dev/null +++ b/tests/components/songpal/__init__.py @@ -0,0 +1,104 @@ +"""Test the songpal integration.""" +from songpal import SongpalException + +from homeassistant.components.songpal.const import CONF_ENDPOINT +from homeassistant.const import CONF_NAME + +from tests.async_mock import AsyncMock, MagicMock, patch + +FRIENDLY_NAME = "name" +ENTITY_ID = f"media_player.{FRIENDLY_NAME}" +HOST = "0.0.0.0" +ENDPOINT = f"http://{HOST}:10000/sony" +MODEL = "model" +MAC = "mac" +SW_VERSION = "sw_ver" + +CONF_DATA = { + CONF_NAME: FRIENDLY_NAME, + CONF_ENDPOINT: ENDPOINT, +} + + +def _create_mocked_device(throw_exception=False): + mocked_device = MagicMock() + + type(mocked_device).get_supported_methods = AsyncMock( + side_effect=SongpalException("Unable to do POST request: ") + if throw_exception + else None + ) + + interface_info = MagicMock() + interface_info.modelName = MODEL + type(mocked_device).get_interface_information = AsyncMock( + return_value=interface_info + ) + + sys_info = MagicMock() + sys_info.macAddr = MAC + sys_info.version = SW_VERSION + type(mocked_device).get_system_info = AsyncMock(return_value=sys_info) + + volume1 = MagicMock() + volume1.maxVolume = 100 + volume1.minVolume = 0 + volume1.volume = 50 + volume1.is_muted = False + volume1.set_volume = AsyncMock() + volume1.set_mute = AsyncMock() + volume2 = MagicMock() + volume2.maxVolume = 100 + volume2.minVolume = 0 + volume2.volume = 20 + volume2.is_muted = True + mocked_device.volume1 = volume1 + type(mocked_device).get_volume_information = AsyncMock( + return_value=[volume1, volume2] + ) + + power = MagicMock() + power.status = True + type(mocked_device).get_power = AsyncMock(return_value=power) + + input1 = MagicMock() + input1.title = "title1" + input1.uri = "uri1" + input1.active = False + input1.activate = AsyncMock() + mocked_device.input1 = input1 + input2 = MagicMock() + input2.title = "title2" + input2.uri = "uri2" + input2.active = True + type(mocked_device).get_inputs = AsyncMock(return_value=[input1, input2]) + + type(mocked_device).set_power = AsyncMock() + type(mocked_device).set_sound_settings = AsyncMock() + type(mocked_device).listen_notifications = AsyncMock() + type(mocked_device).stop_listen_notifications = AsyncMock() + + notification_callbacks = {} + mocked_device.notification_callbacks = notification_callbacks + + def _on_notification(name, callback): + notification_callbacks[name] = callback + + type(mocked_device).on_notification = MagicMock(side_effect=_on_notification) + type(mocked_device).clear_notification_callbacks = MagicMock() + + return mocked_device + + +def _patch_config_flow_device(mocked_device): + return patch( + "homeassistant.components.songpal.config_flow.Device", + return_value=mocked_device, + ) + + +def _patch_media_player_device(mocked_device): + return patch( + "homeassistant.components.songpal.media_player.Device", + return_value=mocked_device, + ) diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py new file mode 100644 index 00000000000..e837ed0e032 --- /dev/null +++ b/tests/components/songpal/test_config_flow.py @@ -0,0 +1,229 @@ +"""Test the songpal config flow.""" +import copy + +from homeassistant.components import ssdp +from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import ( + CONF_DATA, + ENDPOINT, + FRIENDLY_NAME, + HOST, + MODEL, + _create_mocked_device, + _patch_config_flow_device, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +UDN = "uuid:1234" + +SSDP_DATA = { + ssdp.ATTR_UPNP_UDN: UDN, + ssdp.ATTR_UPNP_FRIENDLY_NAME: FRIENDLY_NAME, + ssdp.ATTR_SSDP_LOCATION: f"http://{HOST}:52323/dmr.xml", + "X_ScalarWebAPI_DeviceInfo": { + "X_ScalarWebAPI_BaseURL": ENDPOINT, + "X_ScalarWebAPI_ServiceList": { + "X_ScalarWebAPI_ServiceType": ["guide", "system", "audio", "avContent"], + }, + }, +} + + +def _flow_next(hass, flow_id): + return next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == flow_id + ) + + +def _patch_setup(): + return patch( + "homeassistant.components.songpal.async_setup_entry", return_value=True, + ) + + +async def test_flow_ssdp(hass): + """Test working ssdp flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DATA, + ) + assert result["type"] == "form" + assert result["step_id"] == "init" + assert result["description_placeholders"] == { + CONF_NAME: FRIENDLY_NAME, + CONF_HOST: HOST, + } + flow = _flow_next(hass, result["flow_id"]) + assert flow["context"]["unique_id"] == UDN + + with _patch_setup(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FRIENDLY_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user(hass): + """Test working user initialized flow.""" + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] is None + _flow_next(hass, result["flow_id"]) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MODEL + assert result["data"] == { + CONF_NAME: MODEL, + CONF_ENDPOINT: ENDPOINT, + } + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_called_once() + + +async def test_flow_import(hass): + """Test working import flow.""" + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FRIENDLY_NAME + assert result["data"] == CONF_DATA + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() + + +async def test_flow_import_without_name(hass): + """Test import flow without optional name.""" + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MODEL + assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT} + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_called_once() + + +def _create_mock_config_entry(hass): + MockConfigEntry(domain=DOMAIN, unique_id="uuid:0000", data=CONF_DATA,).add_to_hass( + hass + ) + + +async def test_ssdp_bravia(hass): + """Test discovering a bravia TV.""" + ssdp_data = copy.deepcopy(SSDP_DATA) + ssdp_data["X_ScalarWebAPI_DeviceInfo"]["X_ScalarWebAPI_ServiceList"][ + "X_ScalarWebAPI_ServiceType" + ].append("videoScreen") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=ssdp_data, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "not_songpal_device" + + +async def test_sddp_exist(hass): + """Test discovering existed device.""" + _create_mock_config_entry(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DATA, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_exist(hass): + """Test user adding existed device.""" + mocked_device = _create_mocked_device() + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_called_once() + + +async def test_import_exist(hass): + """Test importing existed device.""" + mocked_device = _create_mocked_device() + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() + + +async def test_user_invalid(hass): + """Test using adding invalid config.""" + mocked_device = _create_mocked_device(True) + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() + + +async def test_import_invalid(hass): + """Test importing invalid config.""" + mocked_device = _create_mocked_device(True) + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() diff --git a/tests/components/songpal/test_init.py b/tests/components/songpal/test_init.py new file mode 100644 index 00000000000..9f5de326cc0 --- /dev/null +++ b/tests/components/songpal/test_init.py @@ -0,0 +1,66 @@ +"""Tests songpal setup.""" +from homeassistant.components import songpal +from homeassistant.setup import async_setup_component + +from . import ( + CONF_DATA, + _create_mocked_device, + _patch_config_flow_device, + _patch_media_player_device, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +def _patch_media_setup(): + """Patch media_player.async_setup_entry.""" + + async def _async_return(): + return True + + return patch( + "homeassistant.components.songpal.media_player.async_setup_entry", + side_effect=_async_return, + ) + + +async def test_setup_empty(hass): + """Test setup without any configuration.""" + with _patch_media_setup() as setup: + assert await async_setup_component(hass, songpal.DOMAIN, {}) is True + await hass.async_block_till_done() + setup.assert_not_called() + + +async def test_setup(hass): + """Test setup the platform.""" + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device), _patch_media_setup() as setup: + assert ( + await async_setup_component( + hass, songpal.DOMAIN, {songpal.DOMAIN: [CONF_DATA]} + ) + is True + ) + await hass.async_block_till_done() + mocked_device.get_supported_methods.assert_called_once() + setup.assert_called_once() + + +async def test_unload(hass): + """Test unload entity.""" + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device), _patch_media_player_device( + mocked_device + ): + assert await async_setup_component(hass, songpal.DOMAIN, {}) is True + await hass.async_block_till_done() + mocked_device.listen_notifications.assert_called_once() + assert await songpal.async_unload_entry(hass, entry) + await hass.async_block_till_done() + mocked_device.stop_listen_notifications.assert_called_once() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py new file mode 100644 index 00000000000..5a7ccfd846d --- /dev/null +++ b/tests/components/songpal/test_media_player.py @@ -0,0 +1,267 @@ +"""Test songpal media_player.""" +from datetime import timedelta +import logging + +from songpal import ( + ConnectChange, + ContentChange, + PowerChange, + SongpalException, + VolumeChange, +) + +from homeassistant.components import media_player, songpal +from homeassistant.components.songpal.const import SET_SOUND_SETTING +from homeassistant.components.songpal.media_player import SUPPORT_SONGPAL +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + CONF_DATA, + CONF_ENDPOINT, + CONF_NAME, + ENDPOINT, + ENTITY_ID, + FRIENDLY_NAME, + MAC, + MODEL, + SW_VERSION, + _create_mocked_device, + _patch_media_player_device, +) + +from tests.async_mock import AsyncMock, MagicMock, call, patch +from tests.common import MockConfigEntry, async_fire_time_changed + + +def _get_attributes(hass): + state = hass.states.get(ENTITY_ID) + return state.as_dict()["attributes"] + + +async def test_setup_platform(hass): + """Test the legacy setup platform.""" + mocked_device = _create_mocked_device(throw_exception=True) + with _patch_media_player_device(mocked_device): + await async_setup_component( + hass, + media_player.DOMAIN, + { + media_player.DOMAIN: [ + { + "platform": songpal.DOMAIN, + CONF_NAME: FRIENDLY_NAME, + CONF_ENDPOINT: ENDPOINT, + } + ], + }, + ) + await hass.async_block_till_done() + + # No device is set up + mocked_device.assert_not_called() + all_states = hass.states.async_all() + assert len(all_states) == 0 + + +async def test_setup_failed(hass, caplog): + """Test failed to set up the entity.""" + mocked_device = _create_mocked_device(throw_exception=True) + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + all_states = hass.states.async_all() + assert len(all_states) == 0 + warning_records = [x for x in caplog.records if x.levelno == logging.WARNING] + assert len(warning_records) == 2 + assert not any(x.levelno == logging.ERROR for x in caplog.records) + caplog.clear() + + utcnow = dt_util.utcnow() + type(mocked_device).get_supported_methods = AsyncMock() + with _patch_media_player_device(mocked_device): + async_fire_time_changed(hass, utcnow + timedelta(seconds=30)) + await hass.async_block_till_done() + all_states = hass.states.async_all() + assert len(all_states) == 1 + assert not any(x.levelno == logging.WARNING for x in caplog.records) + assert not any(x.levelno == logging.ERROR for x in caplog.records) + + +async def test_state(hass): + """Test state of the entity.""" + mocked_device = _create_mocked_device() + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_ON + attributes = state.as_dict()["attributes"] + assert attributes["volume_level"] == 0.5 + assert attributes["is_volume_muted"] is False + assert attributes["source_list"] == ["title1", "title2"] + assert attributes["source"] == "title2" + assert attributes["supported_features"] == SUPPORT_SONGPAL + + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get_device( + identifiers={(songpal.DOMAIN, MAC)}, connections={} + ) + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)} + assert device.manufacturer == "Sony Corporation" + assert device.name == FRIENDLY_NAME + assert device.sw_version == SW_VERSION + assert device.model == MODEL + + entity_registry = await er.async_get_registry(hass) + entity = entity_registry.async_get(ENTITY_ID) + assert entity.unique_id == MAC + + +async def test_services(hass): + """Test services.""" + mocked_device = _create_mocked_device() + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + async def _call(service, **argv): + await hass.services.async_call( + media_player.DOMAIN, + service, + {"entity_id": ENTITY_ID, **argv}, + blocking=True, + ) + + await _call(media_player.SERVICE_TURN_ON) + await _call(media_player.SERVICE_TURN_OFF) + await _call(media_player.SERVICE_TOGGLE) + assert mocked_device.set_power.call_count == 3 + mocked_device.set_power.assert_has_calls([call(True), call(False), call(False)]) + + await _call(media_player.SERVICE_VOLUME_SET, volume_level=0.6) + await _call(media_player.SERVICE_VOLUME_UP) + await _call(media_player.SERVICE_VOLUME_DOWN) + assert mocked_device.volume1.set_volume.call_count == 3 + mocked_device.volume1.set_volume.assert_has_calls( + [call(60), call("+1"), call("-1")] + ) + + await _call(media_player.SERVICE_VOLUME_MUTE, is_volume_muted=True) + mocked_device.volume1.set_mute.assert_called_once_with(True) + + await _call(media_player.SERVICE_SELECT_SOURCE, source="none") + mocked_device.input1.activate.assert_not_called() + await _call(media_player.SERVICE_SELECT_SOURCE, source="title1") + mocked_device.input1.activate.assert_called_once() + + await hass.services.async_call( + songpal.DOMAIN, + SET_SOUND_SETTING, + {"entity_id": ENTITY_ID, "name": "name", "value": "value"}, + blocking=True, + ) + mocked_device.set_sound_settings.assert_called_once_with("name", "value") + mocked_device.set_sound_settings.reset_mock() + + mocked_device2 = _create_mocked_device() + sys_info = MagicMock() + sys_info.macAddr = "mac2" + sys_info.version = SW_VERSION + type(mocked_device2).get_system_info = AsyncMock(return_value=sys_info) + entry2 = MockConfigEntry( + domain=songpal.DOMAIN, data={CONF_NAME: "d2", CONF_ENDPOINT: ENDPOINT} + ) + entry2.add_to_hass(hass) + with _patch_media_player_device(mocked_device2): + await hass.config_entries.async_setup(entry2.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + songpal.DOMAIN, + SET_SOUND_SETTING, + {"entity_id": "all", "name": "name", "value": "value"}, + blocking=True, + ) + mocked_device.set_sound_settings.assert_called_once_with("name", "value") + mocked_device2.set_sound_settings.assert_called_once_with("name", "value") + + +async def test_websocket_events(hass): + """Test websocket events.""" + mocked_device = _create_mocked_device() + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mocked_device.listen_notifications.assert_called_once() + assert mocked_device.on_notification.call_count == 4 + + notification_callbacks = mocked_device.notification_callbacks + + volume_change = MagicMock() + volume_change.mute = True + volume_change.volume = 20 + await notification_callbacks[VolumeChange](volume_change) + attributes = _get_attributes(hass) + assert attributes["is_volume_muted"] is True + assert attributes["volume_level"] == 0.2 + + content_change = MagicMock() + content_change.is_input = False + content_change.uri = "uri1" + await notification_callbacks[ContentChange](content_change) + assert _get_attributes(hass)["source"] == "title2" + content_change.is_input = True + await notification_callbacks[ContentChange](content_change) + assert _get_attributes(hass)["source"] == "title1" + + power_change = MagicMock() + power_change.status = False + await notification_callbacks[PowerChange](power_change) + assert hass.states.get(ENTITY_ID).state == STATE_OFF + + +async def test_disconnected(hass, caplog): + """Test disconnected behavior.""" + mocked_device = _create_mocked_device() + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + async def _assert_state(): + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + connect_change = MagicMock() + connect_change.exception = "disconnected" + type(mocked_device).get_supported_methods = AsyncMock( + side_effect=[SongpalException(""), SongpalException(""), _assert_state] + ) + notification_callbacks = mocked_device.notification_callbacks + with patch("homeassistant.components.songpal.media_player.INITIAL_RETRY_DELAY", 0): + await notification_callbacks[ConnectChange](connect_change) + warning_records = [x for x in caplog.records if x.levelno == logging.WARNING] + assert len(warning_records) == 2 + assert warning_records[0].message.endswith("Got disconnected, trying to reconnect.") + assert warning_records[1].message.endswith("Connection reestablished.") + assert not any(x.levelno == logging.ERROR for x in caplog.records) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 246d1eb1627..20c1eb10320 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,11 +1,11 @@ """Configuration for Sonos tests.""" -from asynctest.mock import Mock, patch as patch import pytest from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS +from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index ea580656b24..d351df32101 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,7 +1,4 @@ """Test the Soundtouch component.""" -from unittest.mock import call - -from asynctest import patch from libsoundtouch.device import ( Config, Preset, @@ -14,6 +11,7 @@ from libsoundtouch.device import ( import pytest from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ) @@ -28,6 +26,8 @@ from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component +from tests.async_mock import call, patch + # pylint: disable=super-init-not-called @@ -255,6 +255,36 @@ class MockStatusPause(Status): self._station_name = None +class MockStatusPlayingAux(Status): + """Mock status AUX.""" + + def __init__(self): + """Init the class.""" + self._source = "AUX" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = None + self._track = None + self._album = None + self._duration = None + self._station_name = None + + +class MockStatusPlayingBluetooth(Status): + """Mock status Bluetooth.""" + + def __init__(self): + """Init the class.""" + self._source = "BLUETOOTH" + self._play_status = "PLAY_STATE" + self._image = "image.url" + self._artist = "artist" + self._track = "track" + self._album = "album" + self._duration = None + self._station_name = None + + async def test_ensure_setup_config(mocked_status, mocked_volume, hass, one_device): """Test setup OK with custom config.""" await setup_soundtouch( @@ -366,6 +396,37 @@ async def test_playing_radio(mocked_status, mocked_volume, hass, one_device): assert entity_1_state.attributes["media_title"] == "station" +async def test_playing_aux(mocked_status, mocked_volume, hass, one_device): + """Test playing AUX info.""" + mocked_status.side_effect = MockStatusPlayingAux + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert one_device.call_count == 1 + assert mocked_status.call_count == 2 + assert mocked_volume.call_count == 2 + + entity_1_state = hass.states.get("media_player.soundtouch_1") + assert entity_1_state.state == STATE_PLAYING + assert entity_1_state.attributes["source"] == "AUX" + + +async def test_playing_bluetooth(mocked_status, mocked_volume, hass, one_device): + """Test playing Bluetooth info.""" + mocked_status.side_effect = MockStatusPlayingBluetooth + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert one_device.call_count == 1 + assert mocked_status.call_count == 2 + assert mocked_volume.call_count == 2 + + entity_1_state = hass.states.get("media_player.soundtouch_1") + assert entity_1_state.state == STATE_PLAYING + assert entity_1_state.attributes["source"] == "BLUETOOTH" + assert entity_1_state.attributes["media_track"] == "track" + assert entity_1_state.attributes["media_artist"] == "artist" + assert entity_1_state.attributes["media_album_name"] == "album" + + async def test_get_volume_level(mocked_status, mocked_volume, hass, one_device): """Test volume level.""" mocked_volume.side_effect = MockVolume @@ -427,7 +488,7 @@ async def test_media_commands(mocked_status, mocked_volume, hass, one_device): assert mocked_volume.call_count == 2 entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["supported_features"] == 18365 + assert entity_1_state.attributes["supported_features"] == 20413 @patch("libsoundtouch.device.SoundTouchDevice.power_off") @@ -695,6 +756,72 @@ async def test_play_media_url( mocked_play_url.assert_called_with("http://fqdn/file.mp3") +@patch("libsoundtouch.device.SoundTouchDevice.select_source_aux") +async def test_select_source_aux( + mocked_select_source_aux, mocked_status, mocked_volume, hass, one_device +): + """Test select AUX.""" + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert mocked_select_source_aux.call_count == 0 + await hass.services.async_call( + "media_player", + "select_source", + {"entity_id": "media_player.soundtouch_1", ATTR_INPUT_SOURCE: "AUX"}, + True, + ) + + assert mocked_select_source_aux.call_count == 1 + + +@patch("libsoundtouch.device.SoundTouchDevice.select_source_bluetooth") +async def test_select_source_bluetooth( + mocked_select_source_bluetooth, mocked_status, mocked_volume, hass, one_device +): + """Test select Bluetooth.""" + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert mocked_select_source_bluetooth.call_count == 0 + await hass.services.async_call( + "media_player", + "select_source", + {"entity_id": "media_player.soundtouch_1", ATTR_INPUT_SOURCE: "BLUETOOTH"}, + True, + ) + + assert mocked_select_source_bluetooth.call_count == 1 + + +@patch("libsoundtouch.device.SoundTouchDevice.select_source_bluetooth") +@patch("libsoundtouch.device.SoundTouchDevice.select_source_aux") +async def test_select_source_invalid_source( + mocked_select_source_aux, + mocked_select_source_bluetooth, + mocked_status, + mocked_volume, + hass, + one_device, +): + """Test select unsupported source.""" + await setup_soundtouch(hass, DEVICE_1_CONFIG) + + assert mocked_select_source_aux.call_count == 0 + assert mocked_select_source_bluetooth.call_count == 0 + + await hass.services.async_call( + "media_player", + "select_source", + { + "entity_id": "media_player.soundtouch_1", + ATTR_INPUT_SOURCE: "SOMETHING_UNSUPPORTED", + }, + True, + ) + + assert mocked_select_source_aux.call_count == 0 + assert mocked_select_source_bluetooth.call_count == 0 + + @patch("libsoundtouch.device.SoundTouchDevice.create_zone") async def test_play_everywhere( mocked_create_zone, mocked_status, mocked_volume, hass, two_zones diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 3644ca462ca..7115151451f 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for the Spotify config flow.""" -from unittest.mock import patch - from spotipy import SpotifyException from homeassistant import data_entry_flow, setup @@ -12,6 +10,7 @@ from homeassistant.components.spotify.const import ( from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 62b7c2bbde2..b6499af5601 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -61,7 +61,7 @@ async def test_scan_not_all_present(hass, aioclient_mock): """Test match fails if some specified attributes are not present.""" aioclient_mock.get( "http://1.1.1.1", - text=f""" + text=""" Paulus @@ -96,7 +96,7 @@ async def test_scan_not_all_match(hass, aioclient_mock): """Test match fails if some specified attribute values differ.""" aioclient_mock.get( "http://1.1.1.1", - text=f""" + text=""" Paulus diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 61a0abb6265..721cf71303d 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta import statistics import unittest -from unittest.mock import patch import pytest @@ -12,6 +11,7 @@ from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CE from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import ( fire_time_changed, get_test_home_assistant, diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 0661a5a9738..dc7892e069a 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,6 +1,4 @@ """The tests for stream.""" -from unittest.mock import MagicMock, patch - import pytest from homeassistant.components.stream.const import ( @@ -14,7 +12,7 @@ from homeassistant.const import CONF_FILENAME from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import AsyncMock, MagicMock, patch async def test_record_service_invalid_file(hass): @@ -76,7 +74,7 @@ async def test_record_service_lookback(hass): hls_mock = MagicMock() hls_mock.num_segments = 3 hls_mock.target_duration = 2 - hls_mock.recv.return_value = mock_coro() + hls_mock.recv = AsyncMock(return_value=None) stream_mock.return_value.outputs = {"hls": hls_mock} # Call Service diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 95eeeecf7ad..fbbeaf0ff44 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,7 +1,6 @@ """The tests for hls streams.""" from datetime import timedelta from io import BytesIO -from unittest.mock import patch import pytest @@ -10,6 +9,7 @@ from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index e04de7e2578..e023814725b 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -1,6 +1,5 @@ """The tests for the Sun component.""" from datetime import datetime, timedelta -from unittest.mock import patch from pytest import mark @@ -10,6 +9,8 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch + async def test_setting_rising(hass): """Test retrieving sun setting and rising.""" diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index fe32fca9cb7..e4e8564f5bb 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,6 +1,5 @@ """The test for switch device automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -11,6 +10,7 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 07ab1d19f55..7a14d3ac117 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -103,3 +103,13 @@ async def test_switch_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomSwitch(switch.SwitchDevice): + pass + + CustomSwitch() + assert "SwitchDevice is deprecated, modify CustomSwitch" in caplog.text diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 2b0150cae67..37d61d6ca19 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -4,7 +4,6 @@ from asyncio import Queue from datetime import datetime from typing import Any, Generator, Optional -from asynctest import CoroutineMock, patch from pytest import fixture from .consts import ( @@ -20,6 +19,8 @@ from .consts import ( DUMMY_REMAINING_TIME, ) +from tests.async_mock import AsyncMock, patch + @patch("aioswitcher.devices.SwitcherV2Device") class MockSwitcherV2Device: @@ -100,7 +101,7 @@ def mock_bridge_fixture() -> Generator[None, Any, None]: await queue.put(MockSwitcherV2Device()) return await queue.get() - mock_bridge = CoroutineMock() + mock_bridge = AsyncMock() patchers = [ patch( @@ -163,9 +164,9 @@ def mock_failed_bridge_fixture() -> Generator[None, Any, None]: @fixture(name="mock_api") -def mock_api_fixture() -> Generator[CoroutineMock, Any, None]: +def mock_api_fixture() -> Generator[AsyncMock, Any, None]: """Fixture for mocking aioswitcher.api.SwitcherV2Api.""" - mock_api = CoroutineMock() + mock_api = AsyncMock() patchers = [ patch( diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 7829a3cc999..41bd42a98b3 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,11 +1,11 @@ """Configure Synology DSM tests.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch -@pytest.fixture(name="dsm_bypass_setup", autouse=True) -def dsm_bypass_setup_fixture(): + +@pytest.fixture(name="bypass_setup", autouse=True) +def bypass_setup_fixture(): """Mock component setup.""" with patch( "homeassistant.components.synology_dsm.async_setup_entry", return_value=True diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 51ee92b1865..f592ad90a88 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the Synology DSM config flow.""" import logging -from unittest.mock import MagicMock, Mock, patch import pytest from synology_dsm.exceptions import ( @@ -18,6 +17,7 @@ from homeassistant.components.synology_dsm.const import ( CONF_VOLUMES, DEFAULT_PORT, DEFAULT_PORT_SSL, + DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN, ) @@ -28,11 +28,13 @@ from homeassistant.const import ( CONF_MAC, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) @@ -307,7 +309,7 @@ async def test_connection_failed(hass: HomeAssistantType, service: MagicMock): async def test_unknown_failed(hass: HomeAssistantType, service: MagicMock): """Test when we have an unknown error.""" - service.return_value.login = Mock(side_effect=SynologyDSMException) + service.return_value.login = Mock(side_effect=SynologyDSMException(None, None)) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -394,3 +396,40 @@ async def test_form_ssdp(hass: HomeAssistantType, service: MagicMock): assert result["data"].get("device_token") is None assert result["data"].get(CONF_DISKS) is None assert result["data"].get(CONF_VOLUMES) is None + + +async def test_options_flow(hass: HomeAssistantType, service: MagicMock): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS, + }, + unique_id=SERIAL, + ) + config_entry.add_to_hass(hass) + + assert config_entry.options == {} + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # Scan interval + # Default + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # Manual + result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SCAN_INTERVAL] == 2 diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 92f0ed9fd16..009701ca886 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -1,11 +1,12 @@ """Test system log component.""" import logging -from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log from homeassistant.core import callback +from tests.async_mock import MagicMock, patch + _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} @@ -139,7 +140,7 @@ async def test_remove_older_logs(hass, hass_client): def log_msg(nr=2): """Log an error at same line.""" - _LOGGER.error(f"error message %s", nr) + _LOGGER.error("error message %s", nr) async def test_dedup_logs(hass, hass_client): diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index fb9156d96d9..2e42a2bc1fb 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Tado config flow.""" -from asynctest import MagicMock, patch import requests from homeassistant import config_entries, setup from homeassistant.components.tado.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry @@ -151,10 +151,18 @@ async def test_form_homekit(hass): await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "homekit"} + DOMAIN, + context={"source": "homekit"}, + data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "form" assert result["errors"] == {} + flow = next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF" entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} @@ -162,6 +170,8 @@ async def test_form_homekit(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "homekit"} + DOMAIN, + context={"source": "homekit"}, + data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, ) assert result["type"] == "abort" diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 2dc16ad79c7..4cde4d9ac31 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -1,11 +1,11 @@ """The tests for the TCP binary sensor platform.""" import unittest -from unittest.mock import Mock, patch from homeassistant.components.tcp import binary_sensor as bin_tcp import homeassistant.components.tcp.sensor as tcp from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant import tests.components.tcp.test_sensor as test_tcp diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index 8e79d4e514d..b06652dc53f 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -2,7 +2,6 @@ from copy import copy import socket import unittest -from unittest.mock import Mock, patch from uuid import uuid4 import homeassistant.components.tcp.sensor as tcp @@ -10,6 +9,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.template import Template from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant TEST_CONFIG = { diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index c8caf28ddf6..095393c8acb 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -30,8 +30,8 @@ _LOGGER = logging.getLogger(__name__) ENTITY_COVER = "cover.test_template_cover" -@pytest.fixture -def calls(hass): +@pytest.fixture(name="calls") +def calls_fixture(hass): """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 8698fddbeab..a59daee9ac6 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -1,6 +1,4 @@ """Test the Tesla config flow.""" -from unittest.mock import patch - from teslajsonpy import TeslaException from homeassistant import config_entries, data_entry_flow, setup @@ -20,7 +18,8 @@ from homeassistant.const import ( HTTP_NOT_FOUND, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_form(hass): @@ -34,11 +33,11 @@ async def test_form(hass): with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=mock_coro(("test-refresh-token", "test-access-token")), + return_value=("test-refresh-token", "test-access-token"), ), patch( - "homeassistant.components.tesla.async_setup", return_value=mock_coro(True) + "homeassistant.components.tesla.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.tesla.async_setup_entry", return_value=mock_coro(True) + "homeassistant.components.tesla.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: "test", CONF_USERNAME: "test@email.com"} @@ -103,7 +102,7 @@ async def test_form_repeat_identifier(hass): ) with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=mock_coro(("test-refresh-token", "test-access-token")), + return_value=("test-refresh-token", "test-access-token"), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -119,7 +118,7 @@ async def test_import(hass): with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=mock_coro(("test-refresh-token", "test-access-token")), + return_value=("test-refresh-token", "test-access-token"), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/tibber/__init__.py b/tests/components/tibber/__init__.py new file mode 100644 index 00000000000..0633a3da06e --- /dev/null +++ b/tests/components/tibber/__init__.py @@ -0,0 +1 @@ +"""Tests for Tibber.""" diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py new file mode 100644 index 00000000000..6b2428fcff9 --- /dev/null +++ b/tests/components/tibber/test_config_flow.py @@ -0,0 +1,69 @@ +"""Tests for Tibber config flow.""" +import pytest + +from homeassistant.components.tibber.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN + +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + + +@pytest.fixture(name="tibber_setup", autouse=True) +def tibber_setup_fixture(): + """Patch tibber setup entry.""" + with patch("homeassistant.components.tibber.async_setup_entry", return_value=True): + yield + + +async def test_show_config_form(hass): + """Test show configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_create_entry(hass): + """Test create entry from user input.""" + test_data = { + CONF_ACCESS_TOKEN: "valid", + } + + unique_user_id = "unique_user_id" + title = "title" + + tibber_mock = MagicMock() + type(tibber_mock).update_info = AsyncMock(return_value=True) + type(tibber_mock).user_id = PropertyMock(return_value=unique_user_id) + type(tibber_mock).name = PropertyMock(return_value=title) + + with patch("tibber.Tibber", return_value=tibber_mock): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "create_entry" + assert result["title"] == title + assert result["data"] == test_data + + +async def test_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists.""" + first_entry = MockConfigEntry( + domain="tibber", data={CONF_ACCESS_TOKEN: "valid"}, unique_id="tibber", + ) + first_entry.add_to_hass(hass) + + test_data = { + CONF_ACCESS_TOKEN: "valid", + } + + with patch("tibber.Tibber.update_info", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index 2aae99f93a5..80a081cd524 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,10 +1,10 @@ """The tests for time_date sensor platform.""" import unittest -from unittest.mock import patch import homeassistant.components.time_date.sensor as time_date import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index dea116b3905..75bafba634f 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from datetime import timedelta import logging -from unittest.mock import patch import pytest @@ -42,6 +41,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 1da0c16d43c..4febd1aa8d1 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,7 +1,6 @@ """Test Times of the Day Binary Sensor.""" from datetime import datetime, timedelta import unittest -from unittest.mock import patch import pytz @@ -12,6 +11,7 @@ from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_ne from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 45d16908446..fdf97243a3a 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -1,7 +1,5 @@ """Tests for the Toon config flow.""" -from unittest.mock import patch - import pytest from toonapilib.toonapilibexceptions import ( AgreementsRetrievalError, @@ -22,6 +20,7 @@ from homeassistant.components.toon.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_APP = { diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index b77198fa9b2..80cfe7a81f8 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,11 +1,10 @@ """Tests for the iCloud config flow.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry USERNAME = "username@me.com" diff --git a/tests/components/tplink/test_common.py b/tests/components/tplink/test_common.py index ef4f1d22a2d..a2bd7ef87ff 100644 --- a/tests/components/tplink/test_common.py +++ b/tests/components/tplink/test_common.py @@ -1,12 +1,13 @@ """Common code tests.""" from datetime import timedelta -from unittest.mock import MagicMock from pyHS100 import SmartDeviceException from homeassistant.components.tplink.common import async_add_entities_retry from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import MagicMock + async def test_async_add_entities_retry(hass: HomeAssistantType): """Test interval callback.""" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 85bf0781864..290151b10cc 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -16,14 +16,12 @@ from homeassistant.components.tplink.common import ( from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockDependency, mock_coro - -MOCK_PYHS100 = MockDependency("pyHS100") +from tests.common import MockConfigEntry, mock_coro async def test_creating_entry_tries_discover(hass): """Test setting up does discovery.""" - with MOCK_PYHS100, patch( + with patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), ) as mock_setup, patch( @@ -47,9 +45,7 @@ async def test_creating_entry_tries_discover(hass): async def test_configuring_tplink_causes_discovery(hass): """Test that specifying empty config does discovery.""" - with MOCK_PYHS100, patch( - "homeassistant.components.tplink.common.Discover.discover" - ) as discover: + with patch("homeassistant.components.tplink.common.Discover.discover") as discover: discover.return_value = {"host": 1234} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -71,6 +67,8 @@ async def test_configuring_device_types(hass, name, cls, platform, count): "homeassistant.components.tplink.common.Discover.discover" ) as discover, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", return_value=True, ): discovery_data = { f"123.123.123.{c}": cls("123.123.123.123") for c in range(count) @@ -175,7 +173,7 @@ async def test_is_dimmable(hass): async def test_configuring_discovery_disabled(hass): """Test that discover does not get called when disabled.""" - with MOCK_PYHS100, patch( + with patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), ) as mock_setup, patch( @@ -224,7 +222,7 @@ async def test_platforms_are_initialized(hass): async def test_no_config_creates_no_entry(hass): """Test for when there is no tplink in config.""" - with MOCK_PYHS100, patch( + with patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), ) as mock_setup: diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 0986362ba28..241789270d7 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,6 +1,5 @@ """Tests for light platform.""" from typing import Callable, NamedTuple -from unittest.mock import Mock, PropertyMock, patch from pyHS100 import SmartDeviceException import pytest @@ -30,6 +29,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, PropertyMock, patch + class LightMockData(NamedTuple): """Mock light data.""" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index a13a3d25a6c..942fd4b01d5 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -1,12 +1,11 @@ """The tests the for Traccar device tracker platform.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components import traccar, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, @@ -16,6 +15,8 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component +from tests.async_mock import patch + HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -23,7 +24,6 @@ HOME_LONGITUDE = -115.815811 @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" - pass @pytest.fixture(name="client") @@ -60,7 +60,9 @@ async def setup_zones(loop, hass): @pytest.fixture(name="webhook_id") async def webhook_id_fixture(hass, client): """Initialize the Traccar component and get the webhook_id.""" - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"external_url": "http://example.com"}, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index d835c06f256..891dd1377fe 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,9 +1,7 @@ """Common tradfri test fixtures.""" -from unittest.mock import patch - import pytest -from tests.common import mock_coro +from tests.async_mock import patch @pytest.fixture @@ -19,5 +17,5 @@ def mock_gateway_info(): def mock_entry_setup(): """Mock entry setup.""" with patch("homeassistant.components.tradfri.async_setup_entry") as mock_setup: - mock_setup.return_value = mock_coro(True) + mock_setup.return_value = True yield mock_setup diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 18fb55eda2f..43e33706bf6 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -1,12 +1,11 @@ """Test the Tradfri config flow.""" -from unittest.mock import patch - import pytest from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry @pytest.fixture @@ -20,9 +19,7 @@ def mock_auth(): async def test_user_connection_successful(hass, mock_auth, mock_entry_setup): """Test a successful connection.""" - mock_auth.side_effect = lambda hass, host, code: mock_coro( - {"host": host, "gateway_id": "bla"} - ) + mock_auth.side_effect = lambda hass, host, code: {"host": host, "gateway_id": "bla"} flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": "user"} @@ -80,12 +77,12 @@ async def test_user_connection_bad_key(hass, mock_auth, mock_entry_setup): async def test_discovery_connection(hass, mock_auth, mock_entry_setup): """Test a connection via discovery.""" - mock_auth.side_effect = lambda hass, host, code: mock_coro( - {"host": host, "gateway_id": "bla"} - ) + mock_auth.side_effect = lambda hass, host, code: {"host": host, "gateway_id": "bla"} flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "zeroconf"}, data={"host": "123.123.123.123"} + "tradfri", + context={"source": "homekit"}, + data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, ) result = await hass.config_entries.flow.async_configure( @@ -95,6 +92,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == "homekit-id" assert result["result"].data == { "host": "123.123.123.123", "gateway_id": "bla", @@ -104,9 +102,12 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): async def test_import_connection(hass, mock_auth, mock_entry_setup): """Test a connection via import.""" - mock_auth.side_effect = lambda hass, host, code: mock_coro( - {"host": host, "gateway_id": "bla", "identity": "mock-iden", "key": "mock-key"} - ) + mock_auth.side_effect = lambda hass, host, code: { + "host": host, + "gateway_id": "bla", + "identity": "mock-iden", + "key": "mock-key", + } flow = await hass.config_entries.flow.async_init( "tradfri", @@ -132,9 +133,12 @@ async def test_import_connection(hass, mock_auth, mock_entry_setup): async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): """Test a connection via import and no groups allowed.""" - mock_auth.side_effect = lambda hass, host, code: mock_coro( - {"host": host, "gateway_id": "bla", "identity": "mock-iden", "key": "mock-key"} - ) + mock_auth.side_effect = lambda hass, host, code: { + "host": host, + "gateway_id": "bla", + "identity": "mock-iden", + "key": "mock-key", + } flow = await hass.config_entries.flow.async_init( "tradfri", @@ -160,9 +164,12 @@ async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): async def test_import_connection_legacy(hass, mock_gateway_info, mock_entry_setup): """Test a connection via import.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: mock_coro( - {"host": host, "identity": identity, "key": key, "gateway_id": "mock-gateway"} - ) + mock_gateway_info.side_effect = lambda hass, host, identity, key: { + "host": host, + "identity": identity, + "key": key, + "gateway_id": "mock-gateway", + } result = await hass.config_entries.flow.async_init( "tradfri", @@ -187,9 +194,12 @@ async def test_import_connection_legacy_no_groups( hass, mock_gateway_info, mock_entry_setup ): """Test a connection via legacy import and no groups allowed.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: mock_coro( - {"host": host, "identity": identity, "key": key, "gateway_id": "mock-gateway"} - ) + mock_gateway_info.side_effect = lambda hass, host, identity, key: { + "host": host, + "identity": identity, + "key": key, + "gateway_id": "mock-gateway", + } result = await hass.config_entries.flow.async_init( "tradfri", @@ -211,16 +221,23 @@ async def test_import_connection_legacy_no_groups( async def test_discovery_duplicate_aborted(hass): - """Test a duplicate discovery host is ignored.""" - MockConfigEntry(domain="tradfri", data={"host": "some-host"}).add_to_hass(hass) + """Test a duplicate discovery host aborts and updates existing entry.""" + entry = MockConfigEntry( + domain="tradfri", data={"host": "some-host"}, unique_id="homekit-id" + ) + entry.add_to_hass(hass) flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "zeroconf"}, data={"host": "some-host"} + "tradfri", + context={"source": "homekit"}, + data={"host": "new-host", "properties": {"id": "homekit-id"}}, ) assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT assert flow["reason"] == "already_configured" + assert entry.data["host"] == "new-host" + async def test_import_duplicate_aborted(hass): """Test a duplicate import host is ignored.""" @@ -237,13 +254,34 @@ async def test_import_duplicate_aborted(hass): async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): """Test a duplicate discovery in progress is ignored.""" result = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "zeroconf"}, data={"host": "123.123.123.123"} + "tradfri", + context={"source": "homekit"}, + data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result2 = await hass.config_entries.flow.async_init( - "tradfri", context={"source": "zeroconf"}, data={"host": "123.123.123.123"} + "tradfri", + context={"source": "homekit"}, + data={"host": "123.123.123.123", "properties": {"id": "homekit-id"}}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_discovery_updates_unique_id(hass): + """Test a duplicate discovery host aborts and updates existing entry.""" + entry = MockConfigEntry(domain="tradfri", data={"host": "some-host"},) + entry.add_to_hass(hass) + + flow = await hass.config_entries.flow.async_init( + "tradfri", + context={"source": "homekit"}, + data={"host": "some-host", "properties": {"id": "homekit-id"}}, + ) + + assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["reason"] == "already_configured" + + assert entry.unique_id == "homekit-id" diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index cf9034df8d6..2845137244b 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,9 +1,8 @@ """Tests for Tradfri setup.""" -from unittest.mock import patch - from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_config_yaml_host_not_imported(hass): @@ -51,9 +50,12 @@ async def test_config_json_host_not_imported(hass): async def test_config_json_host_imported(hass, mock_gateway_info, mock_entry_setup): """Test that we import a configured host.""" - mock_gateway_info.side_effect = lambda hass, host, identity, key: mock_coro( - {"host": host, "identity": identity, "key": key, "gateway_id": "mock-gateway"} - ) + mock_gateway_info.side_effect = lambda hass, host, identity, key: { + "host": host, + "identity": identity, + "key": key, + "gateway_id": "mock-gateway", + } with patch( "homeassistant.components.tradfri.load_json", diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index e4bdd140faa..8ffc25aba5a 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -1,7 +1,6 @@ """Tradfri lights platform tests.""" from copy import deepcopy -from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from pytradfri.device import Device @@ -10,6 +9,7 @@ from pytradfri.device.light_control import LightControl from homeassistant.components import tradfri +from tests.async_mock import MagicMock, Mock, PropertyMock, patch from tests.common import MockConfigEntry DEFAULT_TEST_FEATURES = { diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index bb790b025ea..4436a6adf21 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for Transmission config flow.""" from datetime import timedelta -from unittest.mock import patch import pytest from transmissionrpc.error import TransmissionError @@ -22,6 +21,7 @@ from homeassistant.const import ( CONF_USERNAME, ) +from tests.async_mock import patch from tests.common import MockConfigEntry NAME = "Transmission" diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index 75881e113d7..ab66260a55e 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -1,9 +1,9 @@ """The tests for the Transport NSW (AU) sensor platform.""" import unittest -from unittest.mock import patch from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant VALID_CONFIG = { diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index d78cf793d2f..fc93df0aacf 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -1,10 +1,10 @@ """The test for the Trend sensor platform.""" from datetime import timedelta -from unittest.mock import patch from homeassistant import setup import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 61bb78a0827..527fb559eb1 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,6 +1,5 @@ """The tests for the TTS component.""" import ctypes -from unittest.mock import PropertyMock, patch import pytest import yarl @@ -15,9 +14,11 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.tts as tts from homeassistant.components.tts import _get_cache_files +from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component +from tests.async_mock import PropertyMock, patch from tests.common import assert_setup_component, async_mock_service @@ -84,6 +85,14 @@ def mutagen_mock(): yield +@pytest.fixture(autouse=True) +async def internal_url_mock(hass): + """Mock internal URL of the instance.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) + + async def test_setup_component_demo(hass): """Set up the demo platform with defaults.""" config = {tts.DOMAIN: {"platform": "demo"}} @@ -127,10 +136,9 @@ async def test_setup_component_and_test_service(hass, empty_cache_dir): assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - hass.config.api.base_url + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ) await hass.async_block_till_done() assert ( @@ -160,10 +168,9 @@ async def test_setup_component_and_test_service_with_config_language( ) assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( - hass.config.api.base_url + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" ) await hass.async_block_till_done() assert ( @@ -202,10 +209,9 @@ async def test_setup_component_and_test_service_with_service_language( ) assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( - hass.config.api.base_url + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" ) await hass.async_block_till_done() assert ( @@ -267,10 +273,9 @@ async def test_setup_component_and_test_service_with_service_options( assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( - hass.config.api.base_url, opt_hash + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == f"http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" ) await hass.async_block_till_done() assert ( @@ -305,10 +310,9 @@ async def test_setup_component_and_test_with_service_options_def(hass, empty_cac assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( - hass.config.api.base_url, opt_hash + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == f"http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" ) await hass.async_block_till_done() assert ( @@ -603,10 +607,9 @@ async def test_setup_component_test_with_cache_dir( blocking=True, ) assert len(calls) == 1 - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - hass.config.api.base_url + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ) @@ -662,9 +665,7 @@ async def test_setup_component_and_web_get_url(hass, hass_client): assert req.status == 200 response = await req.json() assert response.get("url") == ( - "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - hass.config.api.base_url - ) + "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py new file mode 100644 index 00000000000..56bfc0867c6 --- /dev/null +++ b/tests/components/tuya/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tuya component.""" diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py new file mode 100644 index 00000000000..eeda68cd2d3 --- /dev/null +++ b/tests/components/tuya/test_config_flow.py @@ -0,0 +1,147 @@ +"""Tests for the Tuya config flow.""" +import pytest +from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME + +from tests.async_mock import Mock, patch +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +COUNTRY_CODE = "1" +TUYA_PLATFORM = "tuya" + +TUYA_USER_DATA = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_COUNTRYCODE: COUNTRY_CODE, + CONF_PLATFORM: TUYA_PLATFORM, +} + + +@pytest.fixture(name="tuya") +def tuya_fixture() -> Mock: + """Patch libraries.""" + with patch("homeassistant.components.tuya.config_flow.TuyaApi") as tuya: + yield tuya + + +async def test_user(hass, tuya): + """Test user config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.tuya.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.tuya.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE + assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM + assert not result["result"].unique_id + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass, tuya): + """Test import step.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.tuya.async_setup", return_value=True, + ) as mock_setup, patch( + "homeassistant.components.tuya.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TUYA_USER_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE + assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM + assert not result["result"].unique_id + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_if_already_setup(hass, tuya): + """Test we abort if Tuya is already setup.""" + MockConfigEntry(domain=DOMAIN, data=TUYA_USER_DATA).add_to_hass(hass) + + # Should fail, config exist (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + # Should fail, config exist (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_abort_on_invalid_credentials(hass, tuya): + """Test when we have invalid credentials.""" + tuya().init.side_effect = TuyaAPIException("Boom") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "auth_failed"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "auth_failed" + + +async def test_abort_on_connection_error(hass, tuya): + """Test when we have a network error.""" + tuya().init.side_effect = TuyaNetException("Boom") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "conn_error" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "conn_error" diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 4c4d499a6d9..185139077df 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,38 +1,35 @@ """Test the init file of Twilio.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components import twilio from homeassistant.core import callback -from tests.common import MockDependency +from tests.async_mock import patch async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up Twilio and sending webhook.""" - with MockDependency("twilio", "rest"), MockDependency("twilio", "twiml"): - with patch("homeassistant.util.get_local_ip", return_value="example.com"): - result = await hass.config_entries.flow.async_init( - "twilio", context={"source": "user"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + with patch("homeassistant.util.get_local_ip", return_value="example.com"): + result = await hass.config_entries.flow.async_init( + "twilio", context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - webhook_id = result["result"].data["webhook_id"] + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + webhook_id = result["result"].data["webhook_id"] - twilio_events = [] + twilio_events = [] - @callback - def handle_event(event): - """Handle Twilio event.""" - twilio_events.append(event) + @callback + def handle_event(event): + """Handle Twilio event.""" + twilio_events.append(event) - hass.bus.async_listen(twilio.RECEIVED_DATA, handle_event) + hass.bus.async_listen(twilio.RECEIVED_DATA, handle_event) - client = await aiohttp_client(hass.http.app) - await client.post(f"/api/webhook/{webhook_id}", data={"hello": "twilio"}) + client = await aiohttp_client(hass.http.app) + await client.post(f"/api/webhook/{webhook_id}", data={"hello": "twilio"}) - assert len(twilio_events) == 1 - assert twilio_events[0].data["webhook_id"] == webhook_id - assert twilio_events[0].data["hello"] == "twilio" + assert len(twilio_events) == 1 + assert twilio_events[0].data["webhook_id"] == webhook_id + assert twilio_events[0].data["hello"] == "twilio" diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 6c656f874d0..3e777fa3d03 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -1,12 +1,12 @@ """The tests for an update of the Twitch component.""" -from unittest.mock import MagicMock, patch - from requests import HTTPError from twitch.resources import Channel, Follow, Stream, Subscription, User from homeassistant.components import sensor from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + ENTITY_ID = "sensor.channel123" CONFIG = { sensor.DOMAIN: { diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index 4979efc22dc..b8eddb9c785 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -2,7 +2,6 @@ import re import unittest -from asynctest import patch import requests_mock from homeassistant.components.uk_transport.sensor import ( @@ -20,6 +19,7 @@ from homeassistant.components.uk_transport.sensor import ( from homeassistant.setup import setup_component from homeassistant.util.dt import now +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture BUS_ATCOCODE = "340000368SHE" diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 189b80c1932..8ce7cef0345 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -1,7 +1,8 @@ """Fixtures for UniFi methods.""" -from asynctest import patch import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def mock_discovery(): diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 1cebb605b5a..a1af12dfb76 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,10 +1,7 @@ """Test UniFi config flow.""" import aiounifi -from asynctest import patch from homeassistant import data_entry_flow -from homeassistant.components import unifi -from homeassistant.components.unifi import config_flow from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_BLOCK_CLIENT, @@ -29,6 +26,7 @@ from homeassistant.const import ( from .test_controller import setup_unifi_integration +from tests.async_mock import patch from tests.common import MockConfigEntry CLIENTS = [{"mac": "00:00:00:00:00:01"}] @@ -52,6 +50,17 @@ DEVICES = [ "radio_name": "wifi1", "wlan_id": "012345678910111213141516", }, + { + "name": "", + "radio": "na", + "radio_name": "wifi1", + "wlan_id": "012345678910111213141516", + }, + { + "radio": "na", + "radio_name": "wifi1", + "wlan_id": "012345678910111213141516", + }, ], } ] @@ -66,7 +75,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): """Test config flow.""" mock_discovery.return_value = "1" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -124,7 +133,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): async def test_flow_works_multiple_sites(hass, aioclient_mock): """Test config flow works when finding multiple sites.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -170,12 +179,12 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): async def test_flow_fails_site_already_configured(hass, aioclient_mock): """Test config flow.""" entry = MockConfigEntry( - domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} + domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -213,56 +222,10 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock): assert result["reason"] == "already_configured" -async def test_flow_fails_site_has_no_local_user(hass, aioclient_mock): - """Test config flow.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - UNIFI_DOMAIN, context={"source": "user"} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - aioclient_mock.get("https://1.2.3.4:1234", status=302) - - aioclient_mock.post( - "https://1.2.3.4:1234/api/login", - json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": "application/json"}, - ) - - aioclient_mock.get( - "https://1.2.3.4:1234/api/self/sites", - json={ - "data": [{"desc": "Site name", "name": "site_id"}], - "meta": {"rc": "ok"}, - }, - headers={"content-type": "application/json"}, - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_VERIFY_SSL: True, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "no_local_user" - - async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): """Test config flow.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -289,7 +252,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): async def test_flow_fails_controller_unavailable(hass, aioclient_mock): """Test config flow.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -316,7 +279,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): async def test_flow_fails_unknown_problem(hass, aioclient_mock): """Test config flow.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + UNIFI_DOMAIN, context={"source": "user"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -339,18 +302,21 @@ async def test_flow_fails_unknown_problem(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_option_flow(hass): - """Test config flow options.""" +async def test_advanced_option_flow(hass): + """Test advanced config flow options.""" controller = await setup_unifi_integration( hass, clients_response=CLIENTS, devices_response=DEVICES, wlans_response=WLANS ) result = await hass.config_entries.options.async_init( - controller.config_entry.entry_id + controller.config_entry.entry_id, context={"show_advanced_options": True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "device_tracker" + assert set( + result["data_schema"].schema[CONF_SSID_FILTER].options.keys() + ).intersection(("SSID 1", "SSID 2", "SSID 2_IOT", "SSID 3")) result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -390,3 +356,33 @@ async def test_option_flow(hass): CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], CONF_ALLOW_BANDWIDTH_SENSORS: True, } + + +async def test_simple_option_flow(hass): + """Test simple config flow options.""" + controller = await setup_unifi_integration( + hass, clients_response=CLIENTS, wlans_response=WLANS + ) + + result = await hass.config_entries.options.async_init( + controller.config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "simple_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], + } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 844eaa5d222..55483d135b6 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -4,15 +4,27 @@ from copy import deepcopy from datetime import timedelta import aiounifi -from asynctest import patch import pytest -from homeassistant.components import unifi +from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_DETECTION_TIME, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DOMAIN as UNIFI_DOMAIN, UNIFI_WIRELESS_CLIENTS, ) +from homeassistant.components.unifi.controller import ( + SUPPORTED_PLATFORMS, + get_controller, +) +from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -22,6 +34,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry CONTROLLER_HOST = { @@ -53,6 +66,7 @@ ENTRY_OPTIONS = {} CONFIGURATION = [] SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}} +DESCRIPTION = [{"name": "username", "site_name": "site_id", "site_role": "admin"}] async def setup_unifi_integration( @@ -60,6 +74,7 @@ async def setup_unifi_integration( config=ENTRY_CONFIG, options=ENTRY_OPTIONS, sites=SITES, + site_description=DESCRIPTION, clients_response=None, devices_response=None, clients_all_response=None, @@ -68,10 +83,10 @@ async def setup_unifi_integration( controllers=None, ): """Create the UniFi controller.""" - assert await async_setup_component(hass, unifi.DOMAIN, {}) + assert await async_setup_component(hass, UNIFI_DOMAIN, {}) config_entry = MockConfigEntry( - domain=unifi.DOMAIN, + domain=UNIFI_DOMAIN, data=deepcopy(config), options=deepcopy(options), entry_id=1, @@ -117,6 +132,8 @@ async def setup_unifi_integration( with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True, ), patch("aiounifi.Controller.sites", return_value=sites), patch( + "aiounifi.Controller.site_description", return_value=site_description + ), patch( "aiounifi.Controller.request", new=mock_request ), patch.object( aiounifi.websocket.WSClient, "start", return_value=True @@ -124,10 +141,9 @@ async def setup_unifi_integration( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - if controller_id not in hass.data[unifi.DOMAIN]: + if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]: return None - controller = hass.data[unifi.DOMAIN][controller_id] + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.mock_client_responses = mock_client_responses controller.mock_device_responses = mock_device_responses @@ -147,31 +163,22 @@ async def test_controller_setup(hass): controller = await setup_unifi_integration(hass) entry = controller.config_entry - assert len(forward_entry_setup.mock_calls) == len( - unifi.controller.SUPPORTED_PLATFORMS - ) - assert forward_entry_setup.mock_calls[0][1] == (entry, "device_tracker") - assert forward_entry_setup.mock_calls[1][1] == (entry, "sensor") - assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") + assert len(forward_entry_setup.mock_calls) == len(SUPPORTED_PLATFORMS) + assert forward_entry_setup.mock_calls[0][1] == (entry, TRACKER_DOMAIN) + assert forward_entry_setup.mock_calls[1][1] == (entry, SENSOR_DOMAIN) + assert forward_entry_setup.mock_calls[2][1] == (entry, SWITCH_DOMAIN) assert controller.host == CONTROLLER_DATA[CONF_HOST] assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] assert controller.site_name in SITES assert controller.site_role == SITES[controller.site_name]["role"] - assert ( - controller.option_allow_bandwidth_sensors - == unifi.const.DEFAULT_ALLOW_BANDWIDTH_SENSORS - ) + assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS assert isinstance(controller.option_block_clients, list) - assert controller.option_track_clients == unifi.const.DEFAULT_TRACK_CLIENTS - assert controller.option_track_devices == unifi.const.DEFAULT_TRACK_DEVICES - assert ( - controller.option_track_wired_clients == unifi.const.DEFAULT_TRACK_WIRED_CLIENTS - ) - assert controller.option_detection_time == timedelta( - seconds=unifi.const.DEFAULT_DETECTION_TIME - ) + assert controller.option_track_clients == DEFAULT_TRACK_CLIENTS + assert controller.option_track_devices == DEFAULT_TRACK_DEVICES + assert controller.option_track_wired_clients == DEFAULT_TRACK_WIRED_CLIENTS + assert controller.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME) assert isinstance(controller.option_ssid_filter, list) assert controller.mac is None @@ -184,23 +191,27 @@ async def test_controller_setup(hass): async def test_controller_mac(hass): """Test that it is possible to identify controller mac.""" controller = await setup_unifi_integration(hass, clients_response=[CONTROLLER_HOST]) - assert controller.mac == "10:00:00:00:00:01" + assert controller.mac == CONTROLLER_HOST["mac"] async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" - with patch.object( - unifi.controller, "get_controller", side_effect=unifi.errors.CannotConnect + with patch( + "homeassistant.components.unifi.controller.get_controller", + side_effect=CannotConnect, ): await setup_unifi_integration(hass) - assert hass.data[unifi.DOMAIN] == {} + assert hass.data[UNIFI_DOMAIN] == {} async def test_controller_unknown_error(hass): """Unknown errors are handled.""" - with patch.object(unifi.controller, "get_controller", side_effect=Exception): + with patch( + "homeassistant.components.unifi.controller.get_controller", + side_effect=Exception, + ): await setup_unifi_integration(hass) - assert hass.data[unifi.DOMAIN] == {} + assert hass.data[UNIFI_DOMAIN] == {} async def test_reset_after_successful_setup(hass): @@ -245,7 +256,7 @@ async def test_get_controller(hass): with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + assert await get_controller(hass, **CONTROLLER_DATA) async def test_get_controller_verify_ssl_false(hass): @@ -255,28 +266,28 @@ async def test_get_controller_verify_ssl_false(hass): with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await unifi.controller.get_controller(hass, **controller_data) + assert await get_controller(hass, **controller_data) async def test_get_controller_login_failed(hass): """Check that get_controller can handle a failed login.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=aiounifi.Unauthorized - ), pytest.raises(unifi.errors.AuthenticationRequired): - await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + ), pytest.raises(AuthenticationRequired): + await get_controller(hass, **CONTROLLER_DATA) async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=aiounifi.RequestError - ), pytest.raises(unifi.errors.CannotConnect): - await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) async def test_get_controller_unknown_error(hass): """Check that get_controller can handle unknown errors.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=aiounifi.AiounifiException - ), pytest.raises(unifi.errors.AuthenticationRequired): - await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + ), pytest.raises(AuthenticationRequired): + await get_controller(hass, **CONTROLLER_DATA) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index c204c75a122..33f296478c8 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,13 +2,17 @@ from copy import copy from datetime import timedelta -from aiounifi.controller import MESSAGE_CLIENT_REMOVED, SIGNAL_CONNECTION_STATE +from aiounifi.controller import ( + MESSAGE_CLIENT, + MESSAGE_CLIENT_REMOVED, + MESSAGE_DEVICE, + MESSAGE_EVENT, + SIGNAL_CONNECTION_STATE, +) from aiounifi.websocket import SIGNAL_DATA, STATE_DISCONNECTED, STATE_RUNNING -from asynctest import patch from homeassistant import config_entries -from homeassistant.components import unifi -import homeassistant.components.device_tracker as device_tracker +from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_IGNORE_WIRED_BUG, @@ -16,6 +20,7 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, + DOMAIN as UNIFI_DOMAIN, ) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry @@ -24,7 +29,10 @@ import homeassistant.util.dt as dt_util from .test_controller import ENTRY_CONFIG, setup_unifi_integration +from tests.common import async_fire_time_changed + CLIENT_1 = { + "ap_mac": "00:00:00:00:02:01", "essid": "ssid", "hostname": "client_1", "ip": "10.0.0.1", @@ -75,6 +83,7 @@ DEVICE_1 = { "mac": "00:00:00:00:01:01", "model": "US16P150", "name": "device_1", + "next_interval": 20, "overheating": True, "state": 1, "type": "usw", @@ -85,35 +94,130 @@ DEVICE_2 = { "board_rev": 3, "device_id": "mock-id", "has_fan": True, - "ip": "10.0.1.1", - "mac": "00:00:00:00:01:01", + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", "model": "US16P150", - "name": "device_1", + "name": "device_2", + "next_interval": 20, "state": 0, "type": "usw", "version": "4.0.42.10433", } +EVENT_CLIENT_1_WIRELESS_CONNECTED = { + "user": CLIENT_1["mac"], + "ssid": CLIENT_1["essid"], + "ap": CLIENT_1["ap_mac"], + "radio": "na", + "channel": "44", + "hostname": CLIENT_1["hostname"], + "key": "EVT_WU_Connected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587753456179, + "datetime": "2020-04-24T18:37:36Z", + "msg": f'User{[CLIENT_1["mac"]]} has connected to AP[{CLIENT_1["ap_mac"]}] with SSID "{CLIENT_1["essid"]}" on "channel 44(na)"', + "_id": "5ea331fa30c49e00f90ddc1a", +} + +EVENT_CLIENT_1_WIRELESS_DISCONNECTED = { + "user": CLIENT_1["mac"], + "ssid": CLIENT_1["essid"], + "hostname": CLIENT_1["hostname"], + "ap": CLIENT_1["ap_mac"], + "duration": 467, + "bytes": 459039, + "key": "EVT_WU_Disconnected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587752927000, + "datetime": "2020-04-24T18:28:47Z", + "msg": f'User{[CLIENT_1["mac"]]} disconnected from "{CLIENT_1["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{CLIENT_1["ap_mac"]}])', + "_id": "5ea32ff730c49e00f90dca1a", +} + +EVENT_DEVICE_2_UPGRADED = { + "_id": "5eae7fe02ab79c00f9d38960", + "datetime": "2020-05-09T20:06:37Z", + "key": "EVT_SW_Upgraded", + "msg": f'Switch[{DEVICE_2["mac"]}] was upgraded from "{DEVICE_2["version"]}" to "4.3.13.11253"', + "subsystem": "lan", + "sw": DEVICE_2["mac"], + "sw_name": DEVICE_2["name"], + "time": 1589054797635, + "version_from": {DEVICE_2["version"]}, + "version_to": "4.3.13.11253", +} + async def test_platform_manually_configured(hass): """Test that nothing happens when configuring unifi through device tracker platform.""" assert ( await async_setup_component( - hass, device_tracker.DOMAIN, {device_tracker.DOMAIN: {"platform": "unifi"}} + hass, TRACKER_DOMAIN, {TRACKER_DOMAIN: {"platform": UNIFI_DOMAIN}} ) is False ) - assert unifi.DOMAIN not in hass.data + assert UNIFI_DOMAIN not in hass.data async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" await setup_unifi_integration(hass) - assert len(hass.states.async_entity_ids("device_tracker")) == 0 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 0 -async def test_tracked_devices(hass): +async def test_tracked_wireless_clients(hass): + """Test the update_items function with some clients.""" + controller = await setup_unifi_integration(hass, clients_response=[CLIENT_1]) + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "not_home" + + # State change signalling works without events + client_1_copy = copy(CLIENT_1) + controller.api.websocket._data = { + "meta": {"message": MESSAGE_CLIENT}, + "data": [client_1_copy], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + # State change signalling works with events + controller.api.websocket._data = { + "meta": {"message": MESSAGE_EVENT}, + "data": [EVENT_CLIENT_1_WIRELESS_DISCONNECTED], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + controller.api.websocket._data = { + "meta": {"message": MESSAGE_EVENT}, + "data": [EVENT_CLIENT_1_WIRELESS_CONNECTED], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + +async def test_tracked_clients(hass): """Test the update_items function with some clients.""" client_4_copy = copy(CLIENT_4) client_4_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) @@ -122,10 +226,9 @@ async def test_tracked_devices(hass): hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, CLIENT_5, client_4_copy], - devices_response=[DEVICE_1, DEVICE_2], known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_entity_ids("device_tracker")) == 5 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 4 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -149,44 +252,88 @@ async def test_tracked_devices(hass): assert client_5 is not None assert client_5.state == "not_home" - device_1 = hass.states.get("device_tracker.device_1") - assert device_1 is not None - assert device_1.state == "not_home" - # State change signalling works client_1_copy = copy(CLIENT_1) - client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "sta:sync"}, "data": [client_1_copy]} - controller.api.message_handler(event) - device_1_copy = copy(DEVICE_1) - device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "device:sync"}, "data": [device_1_copy]} + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]} controller.api.message_handler(event) await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" + +async def test_tracked_devices(hass): + """Test the update_items function with some devices.""" + controller = await setup_unifi_integration( + hass, devices_response=[DEVICE_1, DEVICE_2], + ) + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "not_home" + + # State change signalling work + device_1_copy = copy(DEVICE_1) + device_1_copy["next_interval"] = 20 + event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_1_copy]} + controller.api.message_handler(event) + device_2_copy = copy(DEVICE_2) + device_2_copy["next_interval"] = 50 + event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_2_copy]} + controller.api.message_handler(event) + await hass.async_block_till_done() + device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state == "home" + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=40)) + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "not_home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state == "home" # Disabled device is unavailable device_1_copy = copy(DEVICE_1) device_1_copy["disabled"] = True - event = {"meta": {"message": "device:sync"}, "data": [device_1_copy]} + event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_1_copy]} controller.api.message_handler(event) await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE + # Update device registry when device is upgraded + device_2_copy = copy(DEVICE_2) + device_2_copy["version"] = EVENT_DEVICE_2_UPGRADED["version_to"] + message = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_2_copy]} + controller.api.message_handler(message) + event = {"meta": {"message": MESSAGE_EVENT}, "data": [EVENT_DEVICE_2_UPGRADED]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + # Verify device registry has been updated + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("device_tracker.device_2") + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + assert device.sw_version == EVENT_DEVICE_2_UPGRADED["version_to"] + async def test_remove_clients(hass): """Test the remove_items function with some clients.""" controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2] ) - assert len(hass.states.async_entity_ids("device_tracker")) == 2 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -201,7 +348,7 @@ async def test_remove_clients(hass): controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is None @@ -215,7 +362,7 @@ async def test_controller_state_change(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 2 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 # Controller unavailable controller.async_unifi_signalling_callback( @@ -234,10 +381,10 @@ async def test_controller_state_change(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" + assert client_1.state == "home" device_1 = hass.states.get("device_tracker.device_1") - assert device_1.state == "not_home" + assert device_1.state == "home" async def test_option_track_clients(hass): @@ -245,7 +392,7 @@ async def test_option_track_clients(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 3 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -290,7 +437,7 @@ async def test_option_track_wired_clients(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 3 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -335,7 +482,7 @@ async def test_option_track_devices(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 3 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -376,29 +523,56 @@ async def test_option_track_devices(hass): async def test_option_ssid_filter(hass): - """Test the SSID filter works.""" - controller = await setup_unifi_integration(hass, clients_response=[CLIENT_3]) - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + """Test the SSID filter works. + + Client 1 will travel from a supported SSID to an unsupported ssid. + Client 3 will be removed on change of options since it is in an unsupported SSID. + """ + client_1_copy = copy(CLIENT_1) + client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + + controller = await setup_unifi_integration( + hass, clients_response=[client_1_copy, CLIENT_3] + ) + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" client_3 = hass.states.get("device_tracker.client_3") assert client_3 - # Set SSID filter + # Setting SSID filter will remove clients outside of filter hass.config_entries.async_update_entry( controller.config_entry, options={CONF_SSID_FILTER: ["ssid"]}, ) await hass.async_block_till_done() + # Not affected by SSID filter + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + # Removed due to SSID filter client_3 = hass.states.get("device_tracker.client_3") assert not client_3 + # Roams to SSID outside of filter + client_1_copy = copy(CLIENT_1) + client_1_copy["essid"] = "other_ssid" + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]} + controller.api.message_handler(event) + # Data update while SSID filter is in effect shouldn't create the client client_3_copy = copy(CLIENT_3) client_3_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "sta:sync"}, "data": [client_3_copy]} + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]} controller.api.message_handler(event) await hass.async_block_till_done() - # SSID filter active even though time stamp should mark as home + # SSID filter marks client as away + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + # SSID still outside of filter client_3 = hass.states.get("device_tracker.client_3") assert not client_3 @@ -406,13 +580,37 @@ async def test_option_ssid_filter(hass): hass.config_entries.async_update_entry( controller.config_entry, options={CONF_SSID_FILTER: []}, ) - event = {"meta": {"message": "sta:sync"}, "data": [client_3_copy]} + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]} + controller.api.message_handler(event) + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]} controller.api.message_handler(event) await hass.async_block_till_done() + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "home" + async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + # Client won't go away until after next update + client_3 = hass.states.get("device_tracker.client_3") + assert client_3.state == "home" + + # Trigger update to get client marked as away + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [CLIENT_3]} + controller.api.message_handler(event) + async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) + await hass.async_block_till_done() + + client_3 = hass.states.get("device_tracker.client_3") + assert client_3.state == "not_home" + async def test_wireless_client_go_wired_issue(hass): """Test the solution to catch wireless device go wired UniFi issue. @@ -423,42 +621,51 @@ async def test_wireless_client_go_wired_issue(hass): client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) controller = await setup_unifi_integration(hass, clients_response=[client_1_client]) - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 + # Client is wireless client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None assert client_1.state == "home" assert client_1.attributes["is_wired"] is False + # Trigger wired bug client_1_client["is_wired"] = True - client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]} controller.api.message_handler(event) await hass.async_block_till_done() + # Wired bug fix keeps client marked as wireless client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" assert client_1.attributes["is_wired"] is False - with patch.object( - unifi.device_tracker.dt_util, - "utcnow", - return_value=(dt_util.utcnow() + timedelta(minutes=5)), - ): - event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} - controller.api.message_handler(event) - await hass.async_block_till_done() + # Pass time + async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) + await hass.async_block_till_done() - client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" - assert client_1.attributes["is_wired"] is False + # Marked as home according to the timer + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + assert client_1.attributes["is_wired"] is False - client_1_client["is_wired"] = False - client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + # Try to mark client as connected + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]} controller.api.message_handler(event) await hass.async_block_till_done() + # Make sure it don't go online again until wired bug disappears + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + assert client_1.attributes["is_wired"] is False + + # Make client wireless + client_1_client["is_wired"] = False + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + # Client is no longer affected by wired bug and can be marked online client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" assert client_1.attributes["is_wired"] is False @@ -472,29 +679,51 @@ async def test_option_ignore_wired_bug(hass): controller = await setup_unifi_integration( hass, options={CONF_IGNORE_WIRED_BUG: True}, clients_response=[client_1_client] ) - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 + # Client is wireless client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None assert client_1.state == "home" assert client_1.attributes["is_wired"] is False + # Trigger wired bug client_1_client["is_wired"] = True - client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]} controller.api.message_handler(event) await hass.async_block_till_done() + # Wired bug in effect client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" assert client_1.attributes["is_wired"] is True - client_1_client["is_wired"] = False - client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + # pass time + async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) + await hass.async_block_till_done() + + # Timer marks client as away + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + assert client_1.attributes["is_wired"] is True + + # Mark client as connected again + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]} controller.api.message_handler(event) await hass.async_block_till_done() + # Ignoring wired bug allows client to go home again even while affected + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + assert client_1.attributes["is_wired"] is True + + # Make client wireless + client_1_client["is_wired"] = False + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_client]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + # Client is wireless and still connected client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" assert client_1.attributes["is_wired"] is False @@ -504,7 +733,7 @@ async def test_restoring_client(hass): """Test the update_items function with some clients.""" config_entry = config_entries.ConfigEntry( version=1, - domain=unifi.DOMAIN, + domain=UNIFI_DOMAIN, title="Mock Title", data=ENTRY_CONFIG, source="test", @@ -516,16 +745,16 @@ async def test_restoring_client(hass): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( - device_tracker.DOMAIN, - unifi.DOMAIN, - "{}-site_id".format(CLIENT_1["mac"]), + TRACKER_DOMAIN, + UNIFI_DOMAIN, + f'{CLIENT_1["mac"]}-site_id', suggested_object_id=CLIENT_1["hostname"], config_entry=config_entry, ) registry.async_get_or_create( - device_tracker.DOMAIN, - unifi.DOMAIN, - "{}-site_id".format(CLIENT_2["mac"]), + TRACKER_DOMAIN, + UNIFI_DOMAIN, + f'{CLIENT_2["mac"]}-site_id', suggested_object_id=CLIENT_2["hostname"], config_entry=config_entry, ) @@ -536,7 +765,7 @@ async def test_restoring_client(hass): clients_response=[CLIENT_2], clients_all_response=[CLIENT_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 2 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None @@ -544,52 +773,88 @@ async def test_restoring_client(hass): async def test_dont_track_clients(hass): """Test don't track clients config works.""" - await setup_unifi_integration( + controller = await setup_unifi_integration( hass, - options={unifi.controller.CONF_TRACK_CLIENTS: False}, + options={CONF_TRACK_CLIENTS: False}, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is None device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None - assert device_1.state == "not_home" + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_CLIENTS: True}, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None async def test_dont_track_devices(hass): """Test don't track devices config works.""" - await setup_unifi_integration( + controller = await setup_unifi_integration( hass, - options={unifi.controller.CONF_TRACK_DEVICES: False}, + options={CONF_TRACK_DEVICES: False}, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None - assert client_1.state == "not_home" device_1 = hass.states.get("device_tracker.device_1") assert device_1 is None - -async def test_dont_track_wired_clients(hass): - """Test don't track wired clients config works.""" - await setup_unifi_integration( - hass, - options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, - clients_response=[CLIENT_1, CLIENT_2], + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_DEVICES: True}, ) - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None - assert client_1.state == "not_home" - client_2 = hass.states.get("device_tracker.client_2") + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + + +async def test_dont_track_wired_clients(hass): + """Test don't track wired clients config works.""" + controller = await setup_unifi_integration( + hass, + options={CONF_TRACK_WIRED_CLIENTS: False}, + clients_response=[CLIENT_1, CLIENT_2], + ) + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") assert client_2 is None + + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: True}, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is not None diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 0ccc89cdb89..80e3e07fa17 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,38 +2,40 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi +from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration +from tests.async_mock import AsyncMock from tests.common import MockConfigEntry, mock_coro async def test_setup_with_no_config(hass): """Test that we do not discover anything or try to set up a bridge.""" - assert await async_setup_component(hass, unifi.DOMAIN, {}) is True - assert unifi.DOMAIN not in hass.data + assert await async_setup_component(hass, UNIFI_DOMAIN, {}) is True + assert UNIFI_DOMAIN not in hass.data async def test_successful_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" await setup_unifi_integration(hass) - assert hass.data[unifi.DOMAIN] + assert hass.data[UNIFI_DOMAIN] async def test_controller_fail_setup(hass): """Test that a failed setup still stores controller.""" - with patch.object(unifi, "UniFiController") as mock_cntrlr: - mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) + with patch("homeassistant.components.unifi.UniFiController") as mock_controller: + mock_controller.return_value.async_setup = AsyncMock(return_value=False) await setup_unifi_integration(hass) - assert hass.data[unifi.DOMAIN] == {} + assert hass.data[UNIFI_DOMAIN] == {} async def test_controller_no_mac(hass): """Test that configured options for a host are loaded via config entry.""" entry = MockConfigEntry( - domain=unifi.DOMAIN, + domain=UNIFI_DOMAIN, data={ "controller": { "host": "0.0.0.0", @@ -48,11 +50,13 @@ async def test_controller_no_mac(hass): ) entry.add_to_hass(hass) mock_registry = Mock() - with patch.object(unifi, "UniFiController") as mock_controller, patch( + with patch( + "homeassistant.components.unifi.UniFiController" + ) as mock_controller, patch( "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_coro(mock_registry), ): - mock_controller.return_value.async_setup.return_value = mock_coro(True) + mock_controller.return_value.async_setup = AsyncMock(return_value=True) mock_controller.return_value.mac = None assert await unifi.async_setup_entry(hass, entry) is True @@ -64,7 +68,7 @@ async def test_controller_no_mac(hass): async def test_unload_entry(hass): """Test being able to unload an entry.""" controller = await setup_unifi_integration(hass) - assert hass.data[unifi.DOMAIN] + assert hass.data[UNIFI_DOMAIN] assert await unifi.async_unload_entry(hass, controller.config_entry) - assert not hass.data[unifi.DOMAIN] + assert not hass.data[UNIFI_DOMAIN] diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3c801474235..a768e61468d 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,11 +1,17 @@ """UniFi sensor platform tests.""" from copy import deepcopy -from aiounifi.controller import MESSAGE_CLIENT_REMOVED +from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED from aiounifi.websocket import SIGNAL_DATA -from homeassistant.components import unifi -import homeassistant.components.sensor as sensor +from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.unifi.const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + DOMAIN as UNIFI_DOMAIN, +) from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration @@ -44,21 +50,21 @@ async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a controller.""" assert ( await async_setup_component( - hass, sensor.DOMAIN, {sensor.DOMAIN: {"platform": "unifi"}} + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": UNIFI_DOMAIN}} ) is True ) - assert unifi.DOMAIN not in hass.data + assert UNIFI_DOMAIN not in hass.data async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" controller = await setup_unifi_integration( - hass, options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, + hass, options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("sensor")) == 0 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 async def test_sensors(hass): @@ -66,15 +72,15 @@ async def test_sensors(hass): controller = await setup_unifi_integration( hass, options={ - unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, - unifi.const.CONF_TRACK_CLIENTS: False, - unifi.const.CONF_TRACK_DEVICES: False, + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, }, clients_response=CLIENTS, ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("sensor")) == 4 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx.state == "1234.0" @@ -93,7 +99,7 @@ async def test_sensors(hass): clients[1]["rx_bytes"] = 2345000000 clients[1]["tx_bytes"] = 6789000000 - event = {"meta": {"message": "sta:sync"}, "data": clients} + event = {"meta": {"message": MESSAGE_CLIENT}, "data": clients} controller.api.message_handler(event) await hass.async_block_till_done() @@ -104,8 +110,7 @@ async def test_sensors(hass): assert wireless_client_tx.state == "6789.0" hass.config_entries.async_update_entry( - controller.config_entry, - options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: False}, + controller.config_entry, options={CONF_ALLOW_BANDWIDTH_SENSORS: False}, ) await hass.async_block_till_done() @@ -116,8 +121,7 @@ async def test_sensors(hass): assert wireless_client_tx is None hass.config_entries.async_update_entry( - controller.config_entry, - options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, + controller.config_entry, options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, ) await hass.async_block_till_done() @@ -131,12 +135,10 @@ async def test_sensors(hass): async def test_remove_sensors(hass): """Test the remove_items function with some clients.""" controller = await setup_unifi_integration( - hass, - options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, - clients_response=CLIENTS, + hass, options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, clients_response=CLIENTS, ) - assert len(hass.states.async_entity_ids("sensor")) == 4 - assert len(hass.states.async_entity_ids("device_tracker")) == 2 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx is not None @@ -155,8 +157,8 @@ async def test_remove_sensors(hass): controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("sensor")) == 2 - assert len(hass.states.async_entity_ids("device_tracker")) == 1 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx is None diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 5ea76472739..ea198c6d8f4 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,24 +1,26 @@ -"""UniFi POE control platform tests.""" +"""UniFi switch platform tests.""" from copy import deepcopy -from aiounifi.controller import MESSAGE_CLIENT_REMOVED +from aiounifi.controller import MESSAGE_CLIENT_REMOVED, MESSAGE_EVENT from aiounifi.websocket import SIGNAL_DATA from homeassistant import config_entries -from homeassistant.components import unifi -import homeassistant.components.switch as switch +from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, + DOMAIN as UNIFI_DOMAIN, ) +from homeassistant.components.unifi.switch import POE_SWITCH from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from .test_controller import ( CONTROLLER_HOST, + DESCRIPTION, ENTRY_CONFIG, - SITES, setup_unifi_integration, ) @@ -195,16 +197,69 @@ UNBLOCKED = { "oui": "Producer", } +EVENT_BLOCKED_CLIENT_CONNECTED = { + "user": BLOCKED["mac"], + "radio": "na", + "channel": "44", + "hostname": BLOCKED["hostname"], + "key": "EVT_WU_Connected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587753456179, + "datetime": "2020-04-24T18:37:36Z", + "msg": f'User{[BLOCKED["mac"]]} has connected."', + "_id": "5ea331fa30c49e00f90ddc1a", +} + +EVENT_BLOCKED_CLIENT_BLOCKED = { + "user": BLOCKED["mac"], + "hostname": BLOCKED["hostname"], + "key": "EVT_WC_Blocked", + "subsystem": "wlan", + "site_id": "name", + "time": 1587753456179, + "datetime": "2020-04-24T18:37:36Z", + "msg": f'User{[BLOCKED["mac"]]} has been blocked."', + "_id": "5ea331fa30c49e00f90ddc1a", +} + +EVENT_BLOCKED_CLIENT_UNBLOCKED = { + "user": BLOCKED["mac"], + "hostname": BLOCKED["hostname"], + "key": "EVT_WC_Unblocked", + "subsystem": "wlan", + "site_id": "name", + "time": 1587753456179, + "datetime": "2020-04-24T18:37:36Z", + "msg": f'User{[BLOCKED["mac"]]} has been unblocked."', + "_id": "5ea331fa30c49e00f90ddc1a", +} + + +EVENT_CLIENT_2_CONNECTED = { + "user": CLIENT_2["mac"], + "radio": "na", + "channel": "44", + "hostname": CLIENT_2["hostname"], + "key": "EVT_WU_Connected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587753456179, + "datetime": "2020-04-24T18:37:36Z", + "msg": f'User{[CLIENT_2["mac"]]} has connected."', + "_id": "5ea331fa30c49e00f90ddc1a", +} + async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a controller.""" assert ( await async_setup_component( - hass, switch.DOMAIN, {switch.DOMAIN: {"platform": "unifi"}} + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": UNIFI_DOMAIN}} ) is True ) - assert unifi.DOMAIN not in hass.data + assert UNIFI_DOMAIN not in hass.data async def test_no_clients(hass): @@ -214,7 +269,7 @@ async def test_no_clients(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 async def test_controller_not_client(hass): @@ -227,25 +282,25 @@ async def test_controller_not_client(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None async def test_not_admin(hass): """Test that switch platform only work on an admin account.""" - sites = deepcopy(SITES) - sites["Site name"]["role"] = "not admin" + description = deepcopy(DESCRIPTION) + description[0]["site_role"] = "not admin" controller = await setup_unifi_integration( hass, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - sites=sites, + site_description=description, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 async def test_switches(hass): @@ -263,13 +318,13 @@ async def test_switches(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 3 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None assert switch_1.state == "on" assert switch_1.attributes["power"] == "2.56" - assert switch_1.attributes["switch"] == "00:00:00:00:01:01" + assert switch_1.attributes[SWITCH_DOMAIN] == "00:00:00:00:01:01" assert switch_1.attributes["port"] == 1 assert switch_1.attributes["poe_mode"] == "auto" @@ -285,7 +340,7 @@ async def test_switches(hass): assert unblocked.state == "on" await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True + SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) assert len(controller.mock_requests) == 5 assert controller.mock_requests[4] == { @@ -295,7 +350,7 @@ async def test_switches(hass): } await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True + SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) assert len(controller.mock_requests) == 6 assert controller.mock_requests[5] == { @@ -313,7 +368,7 @@ async def test_remove_switches(hass): clients_response=[CLIENT_1, UNBLOCKED], devices_response=[DEVICE_1], ) - assert len(hass.states.async_entity_ids("switch")) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 poe_switch = hass.states.get("switch.poe_client_1") assert poe_switch is not None @@ -328,7 +383,7 @@ async def test_remove_switches(hass): controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 poe_switch = hass.states.get("switch.poe_client_1") assert poe_switch is None @@ -337,6 +392,74 @@ async def test_remove_switches(hass): assert block_switch is None +async def test_block_switches(hass): + """Test the update_items function with some clients.""" + controller = await setup_unifi_integration( + hass, + options={ + CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + }, + clients_response=[UNBLOCKED], + clients_all_response=[BLOCKED], + ) + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + + blocked = hass.states.get("switch.block_client_1") + assert blocked is not None + assert blocked.state == "off" + + unblocked = hass.states.get("switch.block_client_2") + assert unblocked is not None + assert unblocked.state == "on" + + controller.api.websocket._data = { + "meta": {"message": MESSAGE_EVENT}, + "data": [EVENT_BLOCKED_CLIENT_UNBLOCKED], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + blocked = hass.states.get("switch.block_client_1") + assert blocked is not None + assert blocked.state == "on" + + controller.api.websocket._data = { + "meta": {"message": MESSAGE_EVENT}, + "data": [EVENT_BLOCKED_CLIENT_BLOCKED], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + blocked = hass.states.get("switch.block_client_1") + assert blocked is not None + assert blocked.state == "off" + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 5 + assert controller.mock_requests[4] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, + "method": "post", + "path": "/cmd/stamgr", + } + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 6 + assert controller.mock_requests[5] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, + "method": "post", + "path": "/cmd/stamgr", + } + + async def test_new_client_discovered_on_block_control(hass): """Test if 2nd update has a new client.""" controller = await setup_unifi_integration( @@ -349,7 +472,7 @@ async def test_new_client_discovered_on_block_control(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 blocked = hass.states.get("switch.block_client_1") assert blocked is None @@ -358,10 +481,19 @@ async def test_new_client_discovered_on_block_control(hass): "meta": {"message": "sta:sync"}, "data": [BLOCKED], } - controller.api.session_handler("data") + controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 1 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 + + controller.api.websocket._data = { + "meta": {"message": MESSAGE_EVENT}, + "data": [EVENT_BLOCKED_CLIENT_CONNECTED], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 blocked = hass.states.get("switch.block_client_1") assert blocked is not None @@ -373,7 +505,7 @@ async def test_option_block_clients(hass): options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, clients_all_response=[BLOCKED, UNBLOCKED], ) - assert len(hass.states.async_entity_ids("switch")) == 1 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Add a second switch hass.config_entries.async_update_entry( @@ -381,28 +513,28 @@ async def test_option_block_clients(hass): options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Remove the second switch again hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 1 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Enable one and remove another one hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 1 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 # Remove one hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: []}, ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 0 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 async def test_new_client_discovered_on_poe_control(hass): @@ -415,20 +547,33 @@ async def test_new_client_discovered_on_poe_control(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 1 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 controller.api.websocket._data = { "meta": {"message": "sta:sync"}, "data": [CLIENT_2], } - controller.api.session_handler("data") + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + controller.api.websocket._data = { + "meta": {"message": MESSAGE_EVENT}, + "data": [EVENT_CLIENT_2_CONNECTED], + } + controller.api.session_handler(SIGNAL_DATA) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + switch_2 = hass.states.get("switch.poe_client_2") + assert switch_2 is not None - # Calling a service will trigger the updates to run await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True + SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) assert len(controller.mock_requests) == 5 - assert len(hass.states.async_entity_ids("switch")) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 assert controller.mock_requests[4] == { "json": { "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] @@ -438,7 +583,7 @@ async def test_new_client_discovered_on_poe_control(hass): } await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True + SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True ) assert len(controller.mock_requests) == 6 assert controller.mock_requests[4] == { @@ -451,10 +596,6 @@ async def test_new_client_discovered_on_poe_control(hass): "path": "/rest/device/mock-id", } - switch_2 = hass.states.get("switch.poe_client_2") - assert switch_2 is not None - assert switch_2.state == "on" - async def test_ignore_multiple_poe_clients_on_same_port(hass): """Ignore when there are multiple POE driven clients on same port. @@ -467,7 +608,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("device_tracker")) == 3 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 switch_1 = hass.states.get("switch.poe_client_1") switch_2 = hass.states.get("switch.poe_client_2") @@ -479,7 +620,7 @@ async def test_restoring_client(hass): """Test the update_items function with some clients.""" config_entry = config_entries.ConfigEntry( version=1, - domain=unifi.DOMAIN, + domain=UNIFI_DOMAIN, title="Mock Title", data=ENTRY_CONFIG, source="test", @@ -491,16 +632,16 @@ async def test_restoring_client(hass): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( - switch.DOMAIN, - unifi.DOMAIN, - "poe-{}".format(CLIENT_1["mac"]), + SWITCH_DOMAIN, + UNIFI_DOMAIN, + f'{POE_SWITCH}-{CLIENT_1["mac"]}', suggested_object_id=CLIENT_1["hostname"], config_entry=config_entry, ) registry.async_get_or_create( - switch.DOMAIN, - unifi.DOMAIN, - "poe-{}".format(CLIENT_2["mac"]), + SWITCH_DOMAIN, + UNIFI_DOMAIN, + f'{POE_SWITCH}-{CLIENT_2["mac"]}', suggested_object_id=CLIENT_2["hostname"], config_entry=config_entry, ) @@ -518,7 +659,7 @@ async def test_restoring_client(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids("switch")) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 device_1 = hass.states.get("switch.client_1") assert device_1 is not None diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index 692231d4cad..be58efa415a 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -2,7 +2,6 @@ from datetime import timedelta import os -from asynctest import mock, patch import pytest import voluptuous as vol @@ -23,6 +22,7 @@ from homeassistant.components.unifi_direct.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, load_fixture, mock_component scanner_path = "homeassistant.components.unifi_direct.device_tracker.UnifiDeviceScanner" @@ -38,7 +38,7 @@ def setup_comp(hass): os.remove(yaml_devices) -@patch(scanner_path, return_value=mock.MagicMock(spec=UnifiDeviceScanner)) +@patch(scanner_path, return_value=MagicMock(spec=UnifiDeviceScanner)) async def test_get_scanner(unifi_mock, hass): """Test creating an Unifi direct scanner with a password.""" conf_dict = { @@ -57,7 +57,7 @@ async def test_get_scanner(unifi_mock, hass): assert await async_setup_component(hass, DOMAIN, conf_dict) conf_dict[DOMAIN][CONF_PORT] = 22 - assert unifi_mock.call_args == mock.call(conf_dict[DOMAIN]) + assert unifi_mock.call_args == call(conf_dict[DOMAIN]) @patch("pexpect.pxssh.pxssh") diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index cf3fc8fcb33..bf780d33922 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -22,7 +22,7 @@ def validate_config(config): return validated_config -class MockMediaPlayer(media_player.MediaPlayerDevice): +class MockMediaPlayer(media_player.MediaPlayerEntity): """Mock media player for testing.""" def __init__(self, hass, name): diff --git a/tests/components/upb/__init__.py b/tests/components/upb/__init__.py new file mode 100644 index 00000000000..73840b67797 --- /dev/null +++ b/tests/components/upb/__init__.py @@ -0,0 +1 @@ +"""Tests for the UPB integration.""" diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py new file mode 100644 index 00000000000..9aabcfbfbe7 --- /dev/null +++ b/tests/components/upb/test_config_flow.py @@ -0,0 +1,152 @@ +"""Test the UPB Control config flow.""" + +from asynctest import MagicMock, PropertyMock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.upb.const import DOMAIN + + +def mocked_upb(sync_complete=True, config_ok=True): + """Mock UPB lib.""" + + def _upb_lib_connect(callback): + callback() + + upb_mock = MagicMock() + type(upb_mock).network_id = PropertyMock(return_value="42") + type(upb_mock).config_ok = PropertyMock(return_value=config_ok) + if sync_complete: + upb_mock.connect.side_effect = _upb_lib_connect + return patch( + "homeassistant.components.upb.config_flow.upb_lib.UpbPim", return_value=upb_mock + ) + + +async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): + """Get result dict that are standard for most tests.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with mocked_upb(sync_complete, config_ok), patch( + "homeassistant.components.upb.async_setup_entry", return_value=True + ): + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + {"protocol": "TCP", "address": "1.2.3.4", "file_path": "upb.upe"}, + ) + return result + + +async def test_full_upb_flow_with_serial_port(hass): + """Test a full UPB config flow with serial port.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with mocked_upb(), patch( + "homeassistant.components.upb.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.upb.async_setup_entry", return_value=True + ) as mock_setup_entry: + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + { + "protocol": "Serial port", + "address": "/dev/ttyS0:115200", + "file_path": "upb.upe", + }, + ) + + assert flow["type"] == "form" + assert flow["errors"] == {} + assert result["type"] == "create_entry" + assert result["title"] == "UPB" + assert result["data"] == { + "host": "serial:///dev/ttyS0:115200", + "file_path": "upb.upe", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_tcp_upb(hass): + """Test we can setup a serial upb.""" + result = await valid_tcp_flow(hass) + assert result["type"] == "create_entry" + assert result["data"] == {"host": "tcp://1.2.3.4", "file_path": "upb.upe"} + await hass.async_block_till_done() + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + from asyncio import TimeoutError + + with patch( + "homeassistant.components.upb.config_flow.async_timeout.timeout", + side_effect=TimeoutError, + ): + result = await valid_tcp_flow(hass, sync_complete=False) + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_missing_upb_file(hass): + """Test we handle cannot connect error.""" + result = await valid_tcp_flow(hass, config_ok=False) + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_upb_file"} + + +async def test_form_user_with_already_configured(hass): + """Test we can setup a TCP upb.""" + _ = await valid_tcp_flow(hass) + result2 = await valid_tcp_flow(hass) + assert result2["type"] == "abort" + assert result2["reason"] == "address_already_configured" + await hass.async_block_till_done() + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with mocked_upb(), patch( + "homeassistant.components.upb.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.upb.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": "tcp://42.4.2.42", "file_path": "upb.upe"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "UPB" + + assert result["data"] == {"host": "tcp://42.4.2.42", "file_path": "upb.upe"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_junk_input(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with mocked_upb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"foo": "goo", "goo": "foo"}, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "unknown"} + + await hass.async_block_till_done() diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index e583f4e0114..203a4df8355 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -1,14 +1,12 @@ """The tests for the Updater component.""" -from unittest.mock import Mock - -from asynctest import patch import pytest from homeassistant.components import updater from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_component, mock_coro +from tests.async_mock import patch +from tests.common import mock_component NEW_VERSION = "10000.0" MOCK_VERSION = "10.0" @@ -19,13 +17,6 @@ MOCK_CONFIG = {updater.DOMAIN: {"reporting": True}} RELEASE_NOTES = "test release notes" -@pytest.fixture(autouse=True) -def mock_distro(): - """Mock distro dep.""" - with MockDependency("distro"): - yield - - @pytest.fixture(autouse=True) def mock_version(): """Mock current version.""" @@ -46,7 +37,7 @@ def mock_get_newest_version_fixture(): @pytest.fixture(name="mock_get_uuid", autouse=True) def mock_get_uuid_fixture(): """Fixture to mock get_uuid.""" - with patch("homeassistant.components.updater._load_uuid") as mock: + with patch("homeassistant.helpers.instance_id.async_get") as mock: yield mock @@ -75,7 +66,7 @@ async def test_same_version_shows_entity_false( ): """Test if sensor is false if no new version is available.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + mock_get_newest_version.return_value = (MOCK_VERSION, "") assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) @@ -92,7 +83,7 @@ async def test_same_version_shows_entity_false( async def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): """Test we do not gather analytics when disable reporting is active.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + mock_get_newest_version.return_value = (MOCK_VERSION, "") assert await async_setup_component( hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}} @@ -123,7 +114,7 @@ async def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): with patch( "homeassistant.helpers.system_info.async_get_system_info", - Mock(return_value=mock_coro({"fake": "bla"})), + return_value={"fake": "bla"}, ): res = await updater.get_newest_version(hass, MOCK_HUUID, False) assert res == (MOCK_RESPONSE["version"], MOCK_RESPONSE["release-notes"]) @@ -135,7 +126,7 @@ async def test_error_fetching_new_version_bad_json(hass, aioclient_mock): with patch( "homeassistant.helpers.system_info.async_get_system_info", - Mock(return_value=mock_coro({"fake": "bla"})), + return_value={"fake": "bla"}, ), pytest.raises(UpdateFailed): await updater.get_newest_version(hass, MOCK_HUUID, False) @@ -152,7 +143,7 @@ async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock) with patch( "homeassistant.helpers.system_info.async_get_system_info", - Mock(return_value=mock_coro({"fake": "bla"})), + return_value={"fake": "bla"}, ), pytest.raises(UpdateFailed): await updater.get_newest_version(hass, MOCK_HUUID, False) @@ -163,7 +154,12 @@ async def test_new_version_shows_entity_after_hour_hassio( """Test if binary sensor gets updated if new version is available / Hass.io.""" mock_get_uuid.return_value = MOCK_HUUID mock_component(hass, "hassio") - hass.data["hassio_hass_version"] = "999.0" + hass.data["hassio_info"] = {"hassos": None, "homeassistant": "999.0"} + hass.data["hassio_host"] = { + "supervisor": "222", + "chassis": "vm", + "operating_system": "HassOS 4.6", + } assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) diff --git a/tests/components/upnp/mock_device.py b/tests/components/upnp/mock_device.py new file mode 100644 index 00000000000..17d9b5659c5 --- /dev/null +++ b/tests/components/upnp/mock_device.py @@ -0,0 +1,77 @@ +"""Mock device for testing purposes.""" + +from typing import Mapping + +from homeassistant.components.upnp.const import ( + BYTES_RECEIVED, + BYTES_SENT, + PACKETS_RECEIVED, + PACKETS_SENT, + TIMESTAMP, +) +from homeassistant.components.upnp.device import Device +import homeassistant.util.dt as dt_util + + +class MockDevice(Device): + """Mock device for Device.""" + + def __init__(self, udn): + """Initialize mock device.""" + igd_device = object() + super().__init__(igd_device) + self._udn = udn + self.added_port_mappings = [] + self.removed_port_mappings = [] + + @classmethod + async def async_create_device(cls, hass, ssdp_location): + """Return self.""" + return cls("UDN") + + @property + def udn(self) -> str: + """Get the UDN.""" + return self._udn + + @property + def manufacturer(self) -> str: + """Get manufacturer.""" + return "mock-manufacturer" + + @property + def name(self) -> str: + """Get name.""" + return "mock-name" + + @property + def model_name(self) -> str: + """Get the model name.""" + return "mock-model-name" + + @property + def device_type(self) -> str: + """Get the device type.""" + return "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + + async def _async_add_port_mapping( + self, external_port: int, local_ip: str, internal_port: int + ) -> None: + """Add a port mapping.""" + entry = [external_port, local_ip, internal_port] + self.added_port_mappings.append(entry) + + async def _async_delete_port_mapping(self, external_port: int) -> None: + """Remove a port mapping.""" + entry = external_port + self.removed_port_mappings.append(entry) + + async def async_get_traffic_data(self) -> Mapping[str, any]: + """Get traffic data.""" + return { + TIMESTAMP: dt_util.utcnow(), + BYTES_RECEIVED: 0, + BYTES_SENT: 0, + PACKETS_RECEIVED: 0, + PACKETS_SENT: 0, + } diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py new file mode 100644 index 00000000000..870aa13fc41 --- /dev/null +++ b/tests/components/upnp/test_config_flow.py @@ -0,0 +1,214 @@ +"""Test UPnP/IGD config flow.""" + +from datetime import timedelta + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp +from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_SCAN_INTERVAL, + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DEFAULT_SCAN_INTERVAL, + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, + DOMAIN, +) +from homeassistant.components.upnp.device import Device +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +from .mock_device import MockDevice + +from tests.async_mock import AsyncMock, patch +from tests.common import MockConfigEntry + + +async def test_flow_ssdp_discovery(hass: HomeAssistantType): + """Test config flow: discovered + configured through ssdp.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + discovery_infos = [ + { + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + "friendlyName": mock_device.name, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "ssdp_confirm" + + # Confirm via step ssdp_confirm. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == mock_device.name + assert result["data"] == { + CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_UDN: mock_device.udn, + } + + +async def test_flow_ssdp_discovery_incomplete(hass: HomeAssistantType): + """Test config flow: incomplete discovery through ssdp.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + discovery_infos = [ + { + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_ST: mock_device.device_type, + # ssdp.ATTR_UPNP_UDN: mock_device.udn, # Not provided. + "friendlyName": mock_device.name, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "incomplete_discovery" + + +async def test_flow_user(hass: HomeAssistantType): + """Test config flow: discovered + configured through user.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + usn = f"{mock_device.udn}::{mock_device.device_type}" + discovery_infos = [ + { + DISCOVERY_USN: usn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step user. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # Confirmed via step user. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"usn": usn}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == mock_device.name + assert result["data"] == { + CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_UDN: mock_device.udn, + } + + +async def test_flow_config(hass: HomeAssistantType): + """Test config flow: discovered + configured through configuration.yaml.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + usn = f"{mock_device.udn}::{mock_device.device_type}" + discovery_infos = [ + { + DISCOVERY_USN: usn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step import. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == mock_device.name + assert result["data"] == { + CONFIG_ENTRY_ST: mock_device.device_type, + CONFIG_ENTRY_UDN: mock_device.udn, + } + + +async def test_options_flow(hass: HomeAssistantType): + """Test options flow.""" + # Set up config entry. + udn = "uuid:device_1" + mock_device = MockDevice(udn) + discovery_infos = [ + { + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_LOCATION: "http://192.168.1.1/desc.xml", + } + ] + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONFIG_ENTRY_UDN: mock_device.udn, + CONFIG_ENTRY_ST: mock_device.device_type, + }, + options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + config_entry.add_to_hass(hass) + + config = { + # no upnp, ensures no import-flow is started. + } + async_discover = AsyncMock(return_value=discovery_infos) + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", async_discover): + # Initialisation of component. + await async_setup_component(hass, "upnp", config) + await hass.async_block_till_done() + + # DataUpdateCoordinator gets a default of 30 seconds for updates. + coordinator = hass.data[DOMAIN]["coordinators"][mock_device.udn] + assert coordinator.update_interval == timedelta(seconds=DEFAULT_SCAN_INTERVAL) + + # Options flow with no input results in form. + result = await hass.config_entries.options.async_init(config_entry.entry_id,) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Options flow with input results in update to entry. + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONFIG_ENTRY_SCAN_INTERVAL: 60, + } + + # Also updates DataUpdateCoordinator. + assert coordinator.update_interval == timedelta(seconds=60) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index a2df00aba2d..960d6dacfe5 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,93 +1,49 @@ """Test UPnP/IGD setup process.""" -from ipaddress import IPv4Address -from unittest.mock import patch - from homeassistant.components import upnp +from homeassistant.components.upnp.const import ( + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, +) from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from .mock_device import MockDevice - -class MockDevice(Device): - """Mock device for Device.""" - - def __init__(self, udn): - """Initialize mock device.""" - igd_device = object() - super().__init__(igd_device) - self._udn = udn - self.added_port_mappings = [] - self.removed_port_mappings = [] - - @classmethod - async def async_create_device(cls, hass, ssdp_description): - """Return self.""" - return cls("UDN") - - @property - def udn(self) -> str: - """Get the UDN.""" - return self._udn - - @property - def manufacturer(self) -> str: - """Get manufacturer.""" - return "mock-manufacturer" - - @property - def name(self) -> str: - """Get name.""" - return "mock-name" - - @property - def model_name(self) -> str: - """Get the model name.""" - return "mock-model-name" - - @property - def device_type(self) -> str: - """Get the device type.""" - return "urn:schemas-upnp-org:device:InternetGatewayDevice:1" - - async def _async_add_port_mapping( - self, external_port: int, local_ip: str, internal_port: int - ) -> None: - """Add a port mapping.""" - entry = [external_port, local_ip, internal_port] - self.added_port_mappings.append(entry) - - async def _async_delete_port_mapping(self, external_port: int) -> None: - """Remove a port mapping.""" - entry = external_port - self.removed_port_mappings.append(entry) +from tests.async_mock import AsyncMock, patch +from tests.common import MockConfigEntry async def test_async_setup_entry_default(hass): """Test async_setup_entry.""" udn = "uuid:device_1" - entry = MockConfigEntry(domain=upnp.DOMAIN) + mock_device = MockDevice(udn) + discovery_infos = [ + { + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_LOCATION: "http://192.168.1.1/desc.xml", + } + ] + entry = MockConfigEntry( + domain=upnp.DOMAIN, data={"udn": mock_device.udn, "st": mock_device.device_type} + ) config = { # no upnp } - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover", return_value=mock_coro([]) - ) as async_discover: + async_discover = AsyncMock(return_value=[]) + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", async_discover): + # initialisation of component, no device discovered await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - # mock homeassistant.components.upnp.device.Device - mock_device = MockDevice(udn) - discovery_infos = [ - {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} - ] - - create_device.return_value = mock_coro(return_value=mock_device) - async_discover.return_value = mock_coro(return_value=discovery_infos) - + # loading of config_entry, device discovered + async_discover.return_value = discovery_infos assert await upnp.async_setup_entry(hass, entry) is True # ensure device is stored/used @@ -95,53 +51,3 @@ async def test_async_setup_entry_default(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - - # ensure no port-mappings created or removed - assert not mock_device.added_port_mappings - assert not mock_device.removed_port_mappings - - -async def test_async_setup_entry_port_mapping(hass): - """Test async_setup_entry.""" - # pylint: disable=invalid-name - udn = "uuid:device_1" - entry = MockConfigEntry(domain=upnp.DOMAIN) - - config = { - "http": {}, - "upnp": { - "local_ip": "192.168.1.10", - "port_mapping": True, - "ports": {"hass": "hass"}, - }, - } - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover", return_value=mock_coro([]) - ) as async_discover: - await async_setup_component(hass, "http", config) - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() - - mock_device = MockDevice(udn) - discovery_infos = [ - {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} - ] - - create_device.return_value = mock_coro(return_value=mock_device) - async_discover.return_value = mock_coro(return_value=discovery_infos) - - assert await upnp.async_setup_entry(hass, entry) is True - - # ensure device is stored/used - assert hass.data[upnp.DOMAIN]["devices"][udn] == mock_device - - # ensure add-port-mapping-methods called - assert mock_device.added_port_mappings == [ - [8123, IPv4Address("192.168.1.10"), 8123] - ] - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - # ensure delete-port-mapping-methods called - assert mock_device.removed_port_mappings == [8123] diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index 0a9d227681b..111114d8aca 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -2,11 +2,11 @@ import asyncio from datetime import timedelta import unittest -from unittest.mock import patch from homeassistant.components.uptime.sensor import UptimeSensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 4823d2eb2da..9bd718d1933 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime -from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -32,6 +31,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index f6c1e6c8ead..7116077177a 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,7 +1,6 @@ """The tests for the utility_meter component.""" from datetime import timedelta import logging -from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -19,6 +18,8 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch + _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index b4ee618b54f..09145fc4e4e 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -2,7 +2,6 @@ from contextlib import contextmanager from datetime import timedelta import logging -from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -20,6 +19,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 2b1e9c7782e..1556a165447 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -199,7 +199,10 @@ class TestUVC(unittest.TestCase): "fps": 25, "bitrate": 6000000, "isRtspEnabled": True, - "rtspUris": ["rtsp://host-a:7447/uuid_rtspchannel_0"], + "rtspUris": [ + "rtsp://host-a:7447/uuid_rtspchannel_0", + "rtsp://foo:7447/uuid_rtspchannel_0", + ], }, { "id": "1", @@ -208,7 +211,10 @@ class TestUVC(unittest.TestCase): "fps": 15, "bitrate": 1200000, "isRtspEnabled": False, - "rtspUris": ["rtsp://host-a:7447/uuid_rtspchannel_1"], + "rtspUris": [ + "rtsp://host-a:7447/uuid_rtspchannel_1", + "rtsp://foo:7447/uuid_rtspchannel_1", + ], }, ], } @@ -226,7 +232,7 @@ class TestUVC(unittest.TestCase): def test_stream(self): """Test the RTSP stream URI.""" stream_source = yield from self.uvc.stream_source() - assert stream_source == "rtsp://host-a:7447/uuid_rtspchannel_0" + assert stream_source == "rtsp://foo:7447/uuid_rtspchannel_0" @mock.patch("uvcclient.store.get_info_store") @mock.patch("uvcclient.camera.UVCCameraClientV320") diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py new file mode 100644 index 00000000000..9075b385f40 --- /dev/null +++ b/tests/components/vacuum/test_init.py @@ -0,0 +1,18 @@ +"""The tests for Vacuum.""" +from homeassistant.components import vacuum + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomVacuum(vacuum.VacuumDevice): + pass + + class CustomStateVacuum(vacuum.StateVacuumDevice): + pass + + CustomVacuum() + assert "VacuumDevice is deprecated, modify CustomVacuum" in caplog.text + + CustomStateVacuum() + assert "StateVacuumDevice is deprecated, modify CustomStateVacuum" in caplog.text diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index daeffb4ed1d..3bccacc0a94 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for the Velbus config flow.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow from homeassistant.const import CONF_NAME, CONF_PORT +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry PORT_SERIAL = "/dev/ttyACME100" diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 72651d6eda4..4b0d41d9a1e 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_binary_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 9e5fa983ed0..f52bf375d8e 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.components.climate.const import ( @@ -15,6 +13,8 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_climate( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 52ba55b509c..3915d4d0577 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - from mock import patch from requests.exceptions import RequestException @@ -14,6 +12,7 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) +from tests.async_mock import MagicMock from tests.common import MockConfigEntry @@ -44,8 +43,8 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", CONF_SOURCE: config_entries.SOURCE_USER, - CONF_LIGHTS: ["12", "13"], - CONF_EXCLUDE: ["14", "15"], + CONF_LIGHTS: [12, 13], + CONF_EXCLUDE: [14, 15], } assert result["result"].unique_id == controller.serial_number @@ -154,6 +153,6 @@ async def test_options(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - CONF_LIGHTS: ["1", "2", "3", "4", "5", "6", "7"], - CONF_EXCLUDE: ["8", "9", "10", "11", "12", "13", "14"], + CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], + CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], } diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 62cd47f831c..a2dae2bd7f8 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_cover( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index a6208726451..210037a2ca3 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,14 +1,20 @@ """Vera tests.""" -from asynctest import MagicMock +import pytest import pyvera as pv from requests.exceptions import RequestException -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera import ( + CONF_CONTROLLER, + CONF_EXCLUDE, + CONF_LIGHTS, + DOMAIN, +) from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock from tests.common import MockConfigEntry @@ -110,3 +116,71 @@ async def test_async_setup_entry_error( entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id) + + +@pytest.mark.parametrize( + ["options"], + [ + [{CONF_LIGHTS: [4, 10, 12, "AAA"], CONF_EXCLUDE: [1, "BBB"]}], + [{CONF_LIGHTS: ["4", "10", "12", "AAA"], CONF_EXCLUDE: ["1", "BBB"]}], + ], +) +async def test_exclude_and_light_ids( + hass: HomeAssistant, vera_component_factory: ComponentFactory, options +) -> None: + """Test device exclusion, marking switches as lights and fixing the data type.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "dev1" + vera_device1.is_tripped = False + entity_id1 = "binary_sensor.dev1_1" + + vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device2.device_id = 2 + vera_device2.vera_device_id = 2 + vera_device2.name = "dev2" + vera_device2.is_tripped = False + entity_id2 = "binary_sensor.dev2_2" + + vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch + vera_device3.device_id = 3 + vera_device3.name = "dev3" + vera_device3.category = pv.CATEGORY_SWITCH + vera_device3.is_switched_on = MagicMock(return_value=False) + entity_id3 = "switch.dev3_3" + + vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch + vera_device4.device_id = 4 + vera_device4.name = "dev4" + vera_device4.category = pv.CATEGORY_SWITCH + vera_device4.is_switched_on = MagicMock(return_value=False) + entity_id4 = "light.dev4_4" + + component_data = await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + devices=(vera_device1, vera_device2, vera_device3, vera_device4), + config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options}, + ), + ) + + # Assert the entries were setup correctly. + config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) + assert config_entry.options == { + CONF_LIGHTS: [4, 10, 12], + CONF_EXCLUDE: [1], + } + + update_callback = component_data.controller_data.update_callback + + update_callback(vera_device1) + update_callback(vera_device2) + update_callback(vera_device3) + update_callback(vera_device4) + await hass.async_block_till_done() + + assert hass.states.get(entity_id1) is None + assert hass.states.get(entity_id2) is not None + assert hass.states.get(entity_id3) is not None + assert hass.states.get(entity_id4) is not None diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index fefa07ffa6e..14194d0af52 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR @@ -8,6 +6,8 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_light( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index d1b2209294a..901e09040e9 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -8,6 +6,8 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_lock( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 732a331681b..8f96b7a133a 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_scene( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index c915c5ead0f..cb50ad82789 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -1,6 +1,5 @@ """Vera tests.""" from typing import Any, Callable, Tuple -from unittest.mock import MagicMock import pyvera as pv @@ -9,6 +8,8 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def run_sensor_test( hass: HomeAssistant, diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index c41afad4759..2a8bfe68185 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_switch( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py index 611adde19d9..139ac01a1c6 100644 --- a/tests/components/verisure/test_ethernet_status.py +++ b/tests/components/verisure/test_ethernet_status.py @@ -1,11 +1,12 @@ """Test Verisure ethernet status.""" from contextlib import contextmanager -from unittest.mock import patch from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component +from tests.async_mock import patch + CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index d41bbab2037..decce67dc11 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -1,7 +1,6 @@ """Tests for the Verisure platform.""" from contextlib import contextmanager -from unittest.mock import call, patch from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -12,6 +11,8 @@ from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNLOCKED from homeassistant.setup import async_setup_component +from tests.async_mock import call, patch + NO_DEFAULT_LOCK_CODE_CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 164b4090e5f..471043ae3ae 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,8 +1,8 @@ """The test for the version sensor platform.""" -from unittest.mock import patch - from homeassistant.setup import async_setup_component +from tests.async_mock import patch + MOCK_VERSION = "10.0" diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index 39b847effc5..aedf94da4ab 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,10 +1,9 @@ """Test for vesync config flow.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components.vesync import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index d73d15df8dd..d9e8a2ffd24 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,18 +1,17 @@ """Test the Vilfo Router config flow.""" -from unittest.mock import patch - import vilfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC -from tests.common import mock_coro +from tests.async_mock import patch async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) + mock_mac = "FF-00-00-00-00-00" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -20,12 +19,11 @@ async def test_form(hass): assert result["errors"] == {} with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", return_value=None, - ), patch( - "homeassistant.components.vilfo.async_setup", return_value=mock_coro(True) + "vilfo.Client.get_board_information", return_value=None + ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), patch( + "homeassistant.components.vilfo.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.vilfo.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.vilfo.async_setup_entry" ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -51,6 +49,8 @@ async def test_form_invalid_auth(hass): ) with patch("vilfo.Client.ping", return_value=None), patch( + "vilfo.Client.resolve_mac_address", return_value=None + ), patch( "vilfo.Client.get_board_information", side_effect=vilfo.exceptions.AuthenticationException, ): @@ -69,7 +69,9 @@ async def test_form_cannot_connect(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException): + with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), patch( + "vilfo.Client.resolve_mac_address" + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "testadmin.vilfo.com", "access_token": "test-token"}, @@ -78,7 +80,9 @@ async def test_form_cannot_connect(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} - with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException): + with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), patch( + "vilfo.Client.resolve_mac_address" + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "testadmin.vilfo.com", "access_token": "test-token"}, @@ -107,7 +111,7 @@ async def test_form_already_configured(hass): with patch("vilfo.Client.ping", return_value=None), patch( "vilfo.Client.get_board_information", return_value=None, - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=None): first_flow_result2 = await hass.config_entries.flow.async_configure( first_flow_result1["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -119,7 +123,7 @@ async def test_form_already_configured(hass): with patch("vilfo.Client.ping", return_value=None), patch( "vilfo.Client.get_board_information", return_value=None, - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=None): second_flow_result2 = await hass.config_entries.flow.async_configure( second_flow_result1["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -153,7 +157,7 @@ async def test_validate_input_returns_data(hass): with patch("vilfo.Client.ping", return_value=None), patch( "vilfo.Client.get_board_information", return_value=None - ): + ), patch("vilfo.Client.resolve_mac_address", return_value=None): result = await hass.components.vilfo.config_flow.validate_input( hass, data=mock_data ) diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index e630f201e12..f7448a71c31 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,5 +1,4 @@ """Configure py.test.""" -from asynctest import patch import pytest from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME @@ -21,6 +20,8 @@ from .const import ( MockStartPairingResponse, ) +from tests.async_mock import patch + class MockInput: """Mock Vizio device input.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index b823241ef59..7a2ff1d1c7a 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -3,9 +3,7 @@ from contextlib import asynccontextmanager from datetime import timedelta import logging from typing import Any, Dict, List, Optional -from unittest.mock import call -from asynctest import patch import pytest from pytest import raises from pyvizio.api.apps import AppConfig @@ -74,6 +72,7 @@ from .const import ( VOLUME_STEP, ) +from tests.async_mock import call, patch from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index a65201735ae..6d7dfcf3d7f 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -9,6 +9,7 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) import homeassistant.components.tts as tts +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant, mock_service @@ -22,6 +23,13 @@ class TestTTSVoiceRSSPlatform: """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + asyncio.run_coroutine_threadsafe( + async_process_ha_core_config( + self.hass, {"internal_url": "http://example.local:8123"} + ), + self.hass.loop, + ) + self.url = "https://api.voicerss.org/" self.form_data = { "key": "1234567xx", diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index f57926f30c8..609cdbf6a9e 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -1,7 +1,6 @@ """Test the Vultr binary sensor platform.""" import json import unittest -from unittest.mock import patch import pytest import requests_mock @@ -20,6 +19,7 @@ from homeassistant.components.vultr import ( ) from homeassistant.const import CONF_NAME, CONF_PLATFORM +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py index e371e785c92..6035ac547af 100644 --- a/tests/components/vultr/test_init.py +++ b/tests/components/vultr/test_init.py @@ -2,13 +2,13 @@ from copy import deepcopy import json import unittest -from unittest.mock import patch import requests_mock from homeassistant import setup import homeassistant.components.vultr as vultr +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG = {"vultr": {"api_key": "ABCDEFG1234567"}} diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 80fd05a41cc..1ced0fec82f 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Vultr sensor platform.""" import json import unittest -from unittest.mock import patch import pytest import requests_mock @@ -17,6 +16,7 @@ from homeassistant.const import ( DATA_GIGABYTES, ) +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index 6a5c382a2d2..594617bdfd9 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -1,7 +1,6 @@ """Test the Vultr switch platform.""" import json import unittest -from unittest.mock import patch import pytest import requests_mock @@ -20,6 +19,7 @@ from homeassistant.components.vultr import ( ) from homeassistant.const import CONF_NAME, CONF_PLATFORM +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index c2ee0930895..6eb7afb29f4 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -2,21 +2,18 @@ import pytest import voluptuous as vol -from homeassistant.components import wake_on_lan from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET from homeassistant.setup import async_setup_component -from tests.common import MockDependency +from tests.async_mock import patch async def test_send_magic_packet(hass): """Test of send magic packet service call.""" - with MockDependency("wakeonlan") as mocked_wakeonlan: + with patch("homeassistant.components.wake_on_lan.wakeonlan") as mocked_wakeonlan: mac = "aa:bb:cc:dd:ee:ff" bc_ip = "192.168.255.255" - wake_on_lan.wakeonlan = mocked_wakeonlan - await async_setup_component(hass, DOMAIN, {}) await hass.services.async_call( diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index e0f12f9c7f8..ed4045bcb44 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,11 +1,11 @@ """The tests for the wake on lan switch platform.""" import unittest -from unittest.mock import patch import homeassistant.components.switch as switch from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant, mock_service from tests.components.switch import common diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py new file mode 100644 index 00000000000..967e8b03620 --- /dev/null +++ b/tests/components/water_heater/test_init.py @@ -0,0 +1,12 @@ +"""Tests for Water heater.""" +from homeassistant.components import water_heater + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomWaterHeater(water_heater.WaterHeaterDevice): + pass + + CustomWaterHeater() + assert "WaterHeaterDevice is deprecated, modify CustomWaterHeater" in caplog.text diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 733ed32da78..9051b6325bf 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -1,8 +1,7 @@ """Test the webhook component.""" -from unittest.mock import Mock - import pytest +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component @@ -37,7 +36,9 @@ async def test_unregistering_webhook(hass, mock_client): async def test_generate_webhook_url(hass): """Test we generate a webhook url correctly.""" - hass.config.api = Mock(base_url="https://example.com") + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) url = hass.components.webhook.async_generate_url("some_id") assert url == "https://example.com/api/webhook/some_id" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 9ba35a510dd..099927c6c1f 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -25,9 +25,9 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component if sys.version_info >= (3, 8, 0): - from unittest.mock import patch + from tests.async_mock import patch else: - from asynctest import patch + from tests.async_mock import patch NAME = "fake" @@ -41,8 +41,8 @@ def client_fixture(): "homeassistant.components.webostv.WebOsClient", autospec=True ) as mock_client_class: client = mock_client_class.return_value - client.connection = True client.software_info = {"device_id": "a1:b1:c1:d1:e1:f1"} + client.client_key = "0123456789" yield client diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 9082337ccc8..e76cbe0dbdc 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -2,12 +2,12 @@ from datetime import timedelta from aiohttp import WSMsgType -from asynctest import patch import pytest from homeassistant.components.websocket_api import const, http from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 041c0e76533..2d656de8eeb 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,11 +1,11 @@ """Tests for the Home Assistant Websocket API.""" -from unittest.mock import Mock, patch - from aiohttp import WSMsgType import voluptuous as vol from homeassistant.components.websocket_api import const, messages +from tests.async_mock import Mock, patch + async def test_invalid_message_format(websocket_client): """Test sending invalid JSON.""" diff --git a/tests/components/wiffi/__init__.py b/tests/components/wiffi/__init__.py new file mode 100644 index 00000000000..501fcbb5883 --- /dev/null +++ b/tests/components/wiffi/__init__.py @@ -0,0 +1 @@ +"""Tests for the wiffi integration.""" diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py new file mode 100644 index 00000000000..ef6ce528623 --- /dev/null +++ b/tests/components/wiffi/test_config_flow.py @@ -0,0 +1,109 @@ +"""Test the wiffi integration config flow.""" +import errno + +from asynctest import patch +import pytest + +from homeassistant import config_entries +from homeassistant.components.wiffi.const import DOMAIN +from homeassistant.const import CONF_PORT +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + + +@pytest.fixture(name="dummy_tcp_server") +def mock_dummy_tcp_server(): + """Mock a valid WiffiTcpServer.""" + + class Dummy: + async def start_server(self): + pass + + async def close_server(self): + pass + + server = Dummy() + with patch( + "homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server + ): + yield server + + +@pytest.fixture(name="addr_in_use") +def mock_addr_in_use_server(): + """Mock a WiffiTcpServer with addr_in_use.""" + + class Dummy: + async def start_server(self): + raise OSError(errno.EADDRINUSE, "") + + async def close_server(self): + pass + + server = Dummy() + with patch( + "homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server + ): + yield server + + +@pytest.fixture(name="start_server_failed") +def mock_start_server_failed(): + """Mock a WiffiTcpServer with start_server_failed.""" + + class Dummy: + async def start_server(self): + raise OSError(errno.EACCES, "") + + async def close_server(self): + pass + + server = Dummy() + with patch( + "homeassistant.components.wiffi.config_flow.WiffiTcpServer", return_value=server + ): + yield server + + +async def test_form(hass, dummy_tcp_server): + """Test how we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == config_entries.SOURCE_USER + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PORT: 8765}, + ) + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + + +async def test_form_addr_in_use(hass, addr_in_use): + """Test how we handle addr_in_use error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PORT: 8765}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "addr_in_use" + + +async def test_form_start_server_failed(hass, start_server_failed): + """Test how we handle start_server_failed error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PORT: 8765}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "start_server_failed" diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f57b2d9b0c8..fc30820d5d1 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -124,7 +124,7 @@ async def configure_integration( assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" "response_type=code&client_id=my_client_id&" - "redirect_uri=http://127.0.0.1:8080/auth/external/callback&" + "redirect_uri=http://example.local/auth/external/callback&" f"state={state}" "&scope=user.info,user.metrics,user.activity" ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 65ff65ebbd8..f0528c36005 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,8 +1,6 @@ """Tests for the Withings component.""" from datetime import timedelta -from unittest.mock import patch -from asynctest import MagicMock import pytest from withings_api import WithingsApi from withings_api.common import TimeoutException, UnauthorizedException @@ -14,6 +12,8 @@ from homeassistant.components.withings.common import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.util import dt +from tests.async_mock import MagicMock, patch + @pytest.fixture(name="withings_api") def withings_api_fixture() -> WithingsApi: diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index f6f36ba8eff..7d61c74c50a 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -2,7 +2,6 @@ import re import time -from asynctest import MagicMock import requests_mock import voluptuous as vol from withings_api import AbstractWithingsApi @@ -15,6 +14,7 @@ from homeassistant.components.withings import ( async_setup_entry, const, ) +from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -32,6 +32,8 @@ from .common import ( setup_hass, ) +from tests.async_mock import MagicMock + def config_schema_validate(withings_config) -> None: """Assert a schema config succeeds.""" @@ -163,6 +165,10 @@ async def test_upgrade_token( config = await setup_hass(hass) profiles = config[const.DOMAIN][const.PROFILES] + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local"}, + ) + await configure_integration( hass=hass, aiohttp_client=aiohttp_client, @@ -233,6 +239,10 @@ async def test_auth_failure( config = await setup_hass(hass) profiles = config[const.DOMAIN][const.PROFILES] + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local"}, + ) + await configure_integration( hass=hass, aiohttp_client=aiohttp_client, @@ -268,6 +278,10 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - config = await setup_hass(hass) profiles = config[const.DOMAIN][const.PROFILES] + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local"}, + ) + await configure_integration( hass=hass, aiohttp_client=aiohttp_client, diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 0009677cf18..f2cc5514a2e 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,6 +1,5 @@ """Tests for the WLED light platform.""" import aiohttp -from asynctest.mock import patch from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,6 +31,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index d77bd99b97c..66fedfdd274 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,7 +1,6 @@ """Tests for the WLED sensor platform.""" from datetime import datetime -from asynctest import patch import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -21,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index d140953b948..7d411984cff 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,6 +1,5 @@ """Tests for the WLED switch platform.""" import aiohttp -from asynctest.mock import patch from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -20,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 29d5e4f03ef..c476c9fd0e0 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests the Home Assistant workday binary sensor.""" from datetime import date -from unittest.mock import patch import pytest import voluptuous as vol @@ -8,6 +7,7 @@ import voluptuous as vol import homeassistant.components.workday.binary_sensor as binary_sensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant FUNCTION_PATH = "homeassistant.components.workday.binary_sensor.get_date" diff --git a/tests/components/wwlln/test_config_flow.py b/tests/components/wwlln/test_config_flow.py index e9e32ae75e1..b5d34f542e3 100644 --- a/tests/components/wwlln/test_config_flow.py +++ b/tests/components/wwlln/test_config_flow.py @@ -1,6 +1,4 @@ """Define tests for the WWLLN config flow.""" -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.wwlln import ( CONF_WINDOW, @@ -11,6 +9,7 @@ from homeassistant.components.wwlln import ( from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 5d65aee7616..1760b3274a4 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,7 +1,6 @@ """The tests for the Xiaomi router device tracker platform.""" import logging -from asynctest import mock, patch import requests from homeassistant.components.device_tracker import DOMAIN @@ -9,6 +8,8 @@ import homeassistant.components.xiaomi.device_tracker as xiaomi from homeassistant.components.xiaomi.device_tracker import get_scanner from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from tests.async_mock import MagicMock, call, patch + _LOGGER = logging.getLogger(__name__) INVALID_USERNAME = "bob" @@ -144,7 +145,7 @@ def mocked_requests(*args, **kwargs): @patch( "homeassistant.components.xiaomi.device_tracker.XiaomiDeviceScanner", - return_value=mock.MagicMock(), + return_value=MagicMock(), ) async def test_config(xiaomi_mock, hass): """Testing minimal configuration.""" @@ -159,7 +160,7 @@ async def test_config(xiaomi_mock, hass): } xiaomi.get_scanner(hass, config) assert xiaomi_mock.call_count == 1 - assert xiaomi_mock.call_args == mock.call(config[DOMAIN]) + assert xiaomi_mock.call_args == call(config[DOMAIN]) call_arg = xiaomi_mock.call_args[0][0] assert call_arg["username"] == "admin" assert call_arg["password"] == "passwordTest" @@ -169,7 +170,7 @@ async def test_config(xiaomi_mock, hass): @patch( "homeassistant.components.xiaomi.device_tracker.XiaomiDeviceScanner", - return_value=mock.MagicMock(), + return_value=MagicMock(), ) async def test_config_full(xiaomi_mock, hass): """Testing full configuration.""" @@ -185,7 +186,7 @@ async def test_config_full(xiaomi_mock, hass): } xiaomi.get_scanner(hass, config) assert xiaomi_mock.call_count == 1 - assert xiaomi_mock.call_args == mock.call(config[DOMAIN]) + assert xiaomi_mock.call_args == call(config[DOMAIN]) call_arg = xiaomi_mock.call_args[0][0] assert call_arg["username"] == "alternativeAdminName" assert call_arg["password"] == "passwordTest" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py new file mode 100644 index 00000000000..619293be676 --- /dev/null +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -0,0 +1,125 @@ +"""Test the Xiaomi Miio config flow.""" +from miio import DeviceException + +from homeassistant import config_entries +from homeassistant.components.xiaomi_miio import config_flow, const +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN + +from tests.async_mock import Mock, patch + +TEST_HOST = "1.2.3.4" +TEST_TOKEN = "12345678901234567890123456789012" +TEST_NAME = "Test_Gateway" +TEST_MODEL = "model5" +TEST_MAC = "AB-CD-EF-GH-IJ-KL" +TEST_GATEWAY_ID = f"{TEST_MODEL}-{TEST_MAC}-gateway" +TEST_HARDWARE_VERSION = "AB123" +TEST_FIRMWARE_VERSION = "1.2.3_456" + + +def get_mock_info( + model=TEST_MODEL, + mac_address=TEST_MAC, + hardware_version=TEST_HARDWARE_VERSION, + firmware_version=TEST_FIRMWARE_VERSION, +): + """Return a mock gateway info instance.""" + gateway_info = Mock() + gateway_info.model = model + gateway_info.mac_address = mac_address + gateway_info.hardware_version = hardware_version + gateway_info.firmware_version = firmware_version + + return gateway_info + + +async def test_config_flow_step_user_no_device(hass): + """Test config flow, user step with no device selected.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "no_device_selected"} + + +async def test_config_flow_step_gateway_connect_error(hass): + """Test config flow, gateway connection error.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {config_flow.CONF_GATEWAY: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "gateway" + assert result["errors"] == {} + + with patch( + "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", + side_effect=DeviceException({}), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "gateway" + assert result["errors"] == {"base": "connect_error"} + + +async def test_config_flow_gateway_success(hass): + """Test a successful config flow.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {config_flow.CONF_GATEWAY: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "gateway" + assert result["errors"] == {} + + mock_info = get_mock_info() + + with patch( + "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", + return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + "gateway_id": TEST_GATEWAY_ID, + "model": TEST_MODEL, + "mac": TEST_MAC, + } diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 6b101167c85..2f47ddc7355 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,11 +1,11 @@ """The tests for the Yamaha Media player platform.""" import unittest -from unittest.mock import MagicMock, patch import homeassistant.components.media_player as mp from homeassistant.components.yamaha import media_player as yamaha from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index f0664e4f045..f60e7ba5adf 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -8,12 +8,8 @@ import homeassistant.components.sensor as sensor from homeassistant.const import CONF_NAME import homeassistant.util.dt as dt_util -from tests.common import ( - MockDependency, - assert_setup_component, - async_setup_component, - load_fixture, -) +from tests.async_mock import patch +from tests.common import assert_setup_component, async_setup_component, load_fixture REPLY = json.loads(load_fixture("yandex_transport_reply.json")) @@ -21,8 +17,8 @@ REPLY = json.loads(load_fixture("yandex_transport_reply.json")) @pytest.fixture def mock_requester(): """Create a mock ya_ma module and YandexMapsRequester.""" - with MockDependency("ya_ma") as ya_ma: - instance = ya_ma.YandexMapsRequester.return_value + with patch("ya_ma.YandexMapsRequester") as requester: + instance = requester.return_value instance.get_stop_info.return_value = REPLY yield instance diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index b1bf1bc8ab5..d13ba867cd8 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -8,6 +8,7 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) import homeassistant.components.tts as tts +from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_FORBIDDEN from homeassistant.setup import setup_component @@ -25,6 +26,13 @@ class TestTTSYandexPlatform: self.hass = get_test_home_assistant() self._base_url = "https://tts.voicetech.yandex.net/generate?" + asyncio.run_coroutine_threadsafe( + async_process_ha_core_config( + self.hass, {"internal_url": "http://example.local:8123"} + ), + self.hass.loop, + ) + def teardown_method(self): """Stop everything that was started.""" default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 992185bf102..d940e782be4 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,7 +1,6 @@ """The tests for the notify yessssms platform.""" import logging import unittest -from unittest.mock import patch import pytest import requests_mock @@ -16,6 +15,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(name="config") def config_data(): diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index d676e88bfc3..d8dcbe367de 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the Yr sensor platform.""" from datetime import datetime -from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.const import DEGREE, SPEED_METERS_PER_SECOND, UNIT_PERCENTAGE import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import assert_setup_component, load_fixture NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 6efcd4e463e..89c9d0c2643 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,13 +1,15 @@ """Test Zeroconf component setup process.""" -from unittest.mock import patch - import pytest -from zeroconf import ServiceInfo, ServiceStateChange +from zeroconf import InterfaceChoice, ServiceInfo, ServiceStateChange from homeassistant.components import zeroconf +from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component +from tests.async_mock import patch + NON_UTF8_VALUE = b"ABCDEF\x8a" NON_ASCII_KEY = b"non-ascii-key\x8a" PROPERTIES = { @@ -16,11 +18,14 @@ PROPERTIES = { NON_ASCII_KEY: None, } +HOMEKIT_STATUS_UNPAIRED = b"1" +HOMEKIT_STATUS_PAIRED = b"0" + @pytest.fixture def mock_zeroconf(): """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.Zeroconf") as mock_zc: + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: yield mock_zc.return_value @@ -43,8 +48,8 @@ def get_service_info_mock(service_type, name): ) -def get_homekit_info_mock(model): - """Return homekit info for get_service_info.""" +def get_homekit_info_mock(model, pairing_status): + """Return homekit info for get_service_info for an homekit device.""" def mock_homekit_info(service_type, name): return ServiceInfo( @@ -55,7 +60,7 @@ def get_homekit_info_mock(model): weight=0, priority=0, server="name.local.", - properties={b"md": model.encode()}, + properties={b"md": model.encode(), b"sf": pairing_status}, ) return mock_homekit_info @@ -66,7 +71,7 @@ async def test_setup(hass, mock_zeroconf): with patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -75,7 +80,37 @@ async def test_setup(hass, mock_zeroconf): expected_flow_calls = 0 for matching_components in zc_gen.ZEROCONF.values(): expected_flow_calls += len(matching_components) - assert len(mock_config_flow.mock_calls) == expected_flow_calls * 2 + assert len(mock_config_flow.mock_calls) == expected_flow_calls + + # Test instance is set. + assert "zeroconf" in hass.data + assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf + + +async def test_setup_with_default_interface(hass, mock_zeroconf): + """Test default interface config.""" + with patch.object(hass.config_entries.flow, "async_init"), patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ): + mock_zeroconf.get_service_info.side_effect = get_service_info_mock + assert await async_setup_component( + hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: True}} + ) + + assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default) + + +async def test_setup_without_default_interface(hass, mock_zeroconf): + """Test without default interface config.""" + with patch.object(hass.config_entries.flow, "async_init"), patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ): + mock_zeroconf.get_service_info.side_effect = get_service_info_mock + assert await async_setup_component( + hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} + ) + + assert mock_zeroconf.called_with() async def test_homekit_match_partial_space(hass, mock_zeroconf): @@ -85,13 +120,15 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock("LIFX bulb") + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "LIFX bulb", HOMEKIT_STATUS_UNPAIRED + ) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 2 + assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "lifx" @@ -102,15 +139,15 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "Rachio-fa46ba" + "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED ) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 2 + assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "rachio" @@ -121,14 +158,60 @@ async def test_homekit_match_full(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "ServiceBrowser", side_effect=service_update_mock + zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock("BSB002") + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "BSB002", HOMEKIT_STATUS_UNPAIRED + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + + homekit_mock = get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED) + info = homekit_mock("_hap._tcp.local.", "BSB002._hap._tcp.local.") + import pprint + + pprint.pprint(["homekit", info]) + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "hue" + + +async def test_homekit_already_paired(hass, mock_zeroconf): + """Test that an already paired device is sent to homekit_controller.""" + with patch.dict( + zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "tado", HOMEKIT_STATUS_PAIRED + ) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 2 - assert mock_config_flow.mock_calls[0][1][0] == "hue" + assert mock_config_flow.mock_calls[0][1][0] == "tado" + assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" + + +async def test_homekit_invalid_paring_status(hass, mock_zeroconf): + """Test that missing paring data is not sent to homekit_controller.""" + with patch.dict( + zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "tado", b"invalid" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "tado" async def test_info_from_service_non_utf8(hass): @@ -144,3 +227,11 @@ async def test_info_from_service_non_utf8(hass): assert len(info["properties"]) <= len(raw_info) assert "non-utf8-value" not in info["properties"] assert raw_info["non-utf8-value"] is NON_UTF8_VALUE + + +async def test_get_instance(hass, mock_zeroconf): + """Test we get an instance.""" + assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_zeroconf.ha_close.mock_calls) == 1 diff --git a/tests/components/zerproc/__init__.py b/tests/components/zerproc/__init__.py new file mode 100644 index 00000000000..bd37baed313 --- /dev/null +++ b/tests/components/zerproc/__init__.py @@ -0,0 +1 @@ +"""Tests for the zerproc integration.""" diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py new file mode 100644 index 00000000000..8dbead08adb --- /dev/null +++ b/tests/components/zerproc/test_config_flow.py @@ -0,0 +1,86 @@ +"""Test the zerproc config flow.""" +from asynctest import patch +import pyzerproc + +from homeassistant import config_entries, setup +from homeassistant.components.zerproc.config_flow import DOMAIN + + +async def test_flow_success(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.zerproc.config_flow.pyzerproc.discover", + return_value=["Light1", "Light2"], + ), patch( + "homeassistant.components.zerproc.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zerproc.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Zerproc" + assert result2["data"] == {} + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_no_devices_found(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.zerproc.config_flow.pyzerproc.discover", + return_value=[], + ), patch( + "homeassistant.components.zerproc.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zerproc.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_flow_exceptions_caught(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.zerproc.config_flow.pyzerproc.discover", + side_effect=pyzerproc.ZerprocException("TEST"), + ), patch( + "homeassistant.components.zerproc.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zerproc.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py new file mode 100644 index 00000000000..8f3716a34ea --- /dev/null +++ b/tests/components/zerproc/test_light.py @@ -0,0 +1,316 @@ +"""Test the zerproc lights.""" +from asynctest import patch +import pytest +import pyzerproc + +from homeassistant import setup +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, + SCAN_INTERVAL, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, +) +from homeassistant.components.zerproc.light import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +async def mock_light(hass): + """Create a mock light entity.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_entry = MockConfigEntry(domain=DOMAIN) + mock_entry.add_to_hass(hass) + + light = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + + mock_state = pyzerproc.LightState(False, (0, 0, 0)) + + with patch( + "homeassistant.components.zerproc.light.pyzerproc.discover", + return_value=[light], + ), patch.object(light, "connect"), patch.object( + light, "get_state", return_value=mock_state + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return light + + +async def test_init(hass): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_entry = MockConfigEntry(domain=DOMAIN) + mock_entry.add_to_hass(hass) + + mock_light_1 = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + mock_light_2 = pyzerproc.Light("11:22:33:44:55:66", "LEDBlue-33445566") + + mock_state_1 = pyzerproc.LightState(False, (0, 0, 0)) + mock_state_2 = pyzerproc.LightState(True, (0, 80, 255)) + + with patch( + "homeassistant.components.zerproc.light.pyzerproc.discover", + return_value=[mock_light_1, mock_light_2], + ), patch.object(mock_light_1, "connect"), patch.object( + mock_light_2, "connect" + ), patch.object( + mock_light_1, "get_state", return_value=mock_state_1 + ), patch.object( + mock_light_2, "get_state", return_value=mock_state_2 + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("light.ledblue_ccddeeff") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + } + + state = hass.states.get("light.ledblue_33445566") + assert state.state == STATE_ON + assert state.attributes == { + ATTR_FRIENDLY_NAME: "LEDBlue-33445566", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (221.176, 100.0), + ATTR_RGB_COLOR: (0, 80, 255), + ATTR_XY_COLOR: (0.138, 0.08), + } + + with patch.object(hass.loop, "stop"), patch.object( + mock_light_1, "disconnect" + ) as mock_disconnect_1, patch.object( + mock_light_2, "disconnect" + ) as mock_disconnect_2: + await hass.async_stop() + + assert mock_disconnect_1.called + assert mock_disconnect_2.called + + +async def test_discovery_exception(hass): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_entry = MockConfigEntry(domain=DOMAIN) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.zerproc.light.pyzerproc.discover", + side_effect=pyzerproc.ZerprocException("TEST"), + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # The exception should be captured and no entities should be added + assert len(hass.data[DOMAIN]["addresses"]) == 0 + + +async def test_connect_exception(hass): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_entry = MockConfigEntry(domain=DOMAIN) + mock_entry.add_to_hass(hass) + + mock_light = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + + with patch( + "homeassistant.components.zerproc.light.pyzerproc.discover", + return_value=[mock_light], + ), patch.object( + mock_light, "connect", side_effect=pyzerproc.ZerprocException("TEST") + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # The exception should be captured and no entities should be added + assert len(hass.data[DOMAIN]["addresses"]) == 0 + + +async def test_light_turn_on(hass, mock_light): + """Test ZerprocLight turn_on.""" + utcnow = dt_util.utcnow() + with patch.object(mock_light, "turn_on") as mock_turn_on: + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.ledblue_ccddeeff"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_turn_on.assert_called() + + with patch.object(mock_light, "set_color") as mock_set_color: + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.ledblue_ccddeeff", ATTR_BRIGHTNESS: 25}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(25, 25, 25) + + # Make sure no discovery calls are made while we emulate time passing + with patch("homeassistant.components.zerproc.light.pyzerproc.discover"): + with patch.object( + mock_light, + "get_state", + return_value=pyzerproc.LightState(True, (175, 150, 220)), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + with patch.object(mock_light, "set_color") as mock_set_color: + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.ledblue_ccddeeff", ATTR_BRIGHTNESS: 25}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(19, 17, 25) + + with patch.object(mock_light, "set_color") as mock_set_color: + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.ledblue_ccddeeff", ATTR_HS_COLOR: (50, 50)}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(220, 201, 110) + + with patch.object( + mock_light, + "get_state", + return_value=pyzerproc.LightState(True, (75, 75, 75)), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + with patch.object(mock_light, "set_color") as mock_set_color: + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.ledblue_ccddeeff", ATTR_HS_COLOR: (50, 50)}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(75, 68, 37) + + with patch.object(mock_light, "set_color") as mock_set_color: + await hass.services.async_call( + "light", + "turn_on", + { + ATTR_ENTITY_ID: "light.ledblue_ccddeeff", + ATTR_BRIGHTNESS: 200, + ATTR_HS_COLOR: (75, 75), + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(162, 200, 50) + + +async def test_light_turn_off(hass, mock_light): + """Test ZerprocLight turn_on.""" + with patch.object(mock_light, "turn_off") as mock_turn_off: + await hass.services.async_call( + "light", + "turn_off", + {ATTR_ENTITY_ID: "light.ledblue_ccddeeff"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_turn_off.assert_called() + + +async def test_light_update(hass, mock_light): + """Test ZerprocLight update.""" + utcnow = dt_util.utcnow() + + state = hass.states.get("light.ledblue_ccddeeff") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + } + + # Make sure no discovery calls are made while we emulate time passing + with patch("homeassistant.components.zerproc.light.pyzerproc.discover"): + # Test an exception during discovery + with patch.object( + mock_light, "get_state", side_effect=pyzerproc.ZerprocException("TEST") + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.ledblue_ccddeeff") + assert state.state == STATE_UNAVAILABLE + assert state.attributes == { + ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + } + + with patch.object( + mock_light, + "get_state", + return_value=pyzerproc.LightState(False, (200, 128, 100)), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.ledblue_ccddeeff") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + } + + with patch.object( + mock_light, + "get_state", + return_value=pyzerproc.LightState(True, (175, 150, 220)), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.ledblue_ccddeeff") + assert state.state == STATE_ON + assert state.attributes == { + ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + ATTR_BRIGHTNESS: 220, + ATTR_HS_COLOR: (261.429, 31.818), + ATTR_RGB_COLOR: (202, 173, 255), + ATTR_XY_COLOR: (0.291, 0.232), + } diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 2542037a2bf..f10ee25018f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,8 +1,6 @@ """Common test objects.""" import time -from unittest.mock import Mock -from asynctest import CoroutineMock from zigpy.device import Device as zigpy_dev from zigpy.endpoint import Endpoint as zigpy_ep import zigpy.profiles.zha @@ -15,6 +13,8 @@ import zigpy.zdo.types import homeassistant.components.zha.core.const as zha_const from homeassistant.util import slugify +from tests.async_mock import AsyncMock, Mock + class FakeEndpoint: """Fake endpoint for moking zigpy.""" @@ -32,7 +32,7 @@ class FakeEndpoint: self.model = model self.profile_id = zigpy.profiles.zha.PROFILE_ID self.device_type = None - self.request = CoroutineMock() + self.request = AsyncMock(return_value=[0]) def add_input_cluster(self, cluster_id): """Add an input cluster.""" @@ -48,6 +48,9 @@ class FakeEndpoint: patch_cluster(cluster) self.out_clusters[cluster_id] = cluster + reply = AsyncMock(return_value=[0]) + request = AsyncMock(return_value=[0]) + @property def __class__(self): """Fake being Zigpy endpoint.""" @@ -60,20 +63,21 @@ class FakeEndpoint: FakeEndpoint.add_to_group = zigpy_ep.add_to_group +FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group def patch_cluster(cluster): """Patch a cluster for testing.""" - cluster.bind = CoroutineMock(return_value=[0]) - cluster.configure_reporting = CoroutineMock(return_value=[0]) + cluster.bind = AsyncMock(return_value=[0]) + cluster.configure_reporting = AsyncMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() - cluster.read_attributes = CoroutineMock(return_value=[{}, {}]) + cluster.read_attributes = AsyncMock(return_value=[{}, {}]) cluster.read_attributes_raw = Mock() - cluster.unbind = CoroutineMock(return_value=[0]) - cluster.write_attributes = CoroutineMock(return_value=[0]) + cluster.unbind = AsyncMock(return_value=[0]) + cluster.write_attributes = AsyncMock(return_value=[0]) if cluster.cluster_id == 4: - cluster.add = CoroutineMock(return_value=[0]) + cluster.add = AsyncMock(return_value=[0]) class FakeDevice: @@ -96,7 +100,7 @@ class FakeDevice: self.manufacturer = manufacturer self.model = model self.node_desc = zigpy.zdo.types.NodeDescriptor() - self.remove_from_group = CoroutineMock() + self.remove_from_group = AsyncMock() if node_desc is None: node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0] @@ -135,6 +139,7 @@ async def send_attributes_report(hass, cluster: int, attributes: dict): """ attrs = [make_attribute(attrid, value) for attrid, value in attributes.items()] hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + hdr.frame_control.disable_default_response = True cluster.handle_message(hdr, [attrs]) await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b83db53533c..6df46273354 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,20 +1,19 @@ """Test configuration for the ZHA component.""" -from unittest import mock -import asynctest import pytest import zigpy from zigpy.application import ControllerApplication +import zigpy.config import zigpy.group import zigpy.types import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device -import homeassistant.components.zha.core.registries as zha_regs from homeassistant.setup import async_setup_component from .common import FakeDevice, FakeEndpoint, get_zha_gateway +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry FIXTURE_GRP_ID = 0x1001 @@ -24,37 +23,28 @@ FIXTURE_GRP_NAME = "fixture group" @pytest.fixture def zigpy_app_controller(): """Zigpy ApplicationController fixture.""" - app = mock.MagicMock(spec_set=ControllerApplication) - app.startup = asynctest.CoroutineMock() - app.shutdown = asynctest.CoroutineMock() + app = MagicMock(spec_set=ControllerApplication) + app.startup = AsyncMock() + app.shutdown = AsyncMock() groups = zigpy.group.Groups(app) groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) app.configure_mock(groups=groups) - type(app).ieee = mock.PropertyMock() + type(app).ieee = PropertyMock() app.ieee.return_value = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") - type(app).nwk = mock.PropertyMock(return_value=zigpy.types.NWK(0x0000)) - type(app).devices = mock.PropertyMock(return_value={}) + type(app).nwk = PropertyMock(return_value=zigpy.types.NWK(0x0000)) + type(app).devices = PropertyMock(return_value={}) return app -@pytest.fixture -def zigpy_radio(): - """Zigpy radio mock.""" - radio = mock.MagicMock() - radio.connect = asynctest.CoroutineMock() - return radio - - @pytest.fixture(name="config_entry") async def config_entry_fixture(hass): """Fixture representing a config entry.""" entry = MockConfigEntry( - version=1, + version=2, domain=zha_const.DOMAIN, data={ - zha_const.CONF_BAUDRATE: zha_const.DEFAULT_BAUDRATE, - zha_const.CONF_RADIO_TYPE: "MockRadio", - zha_const.CONF_USB_PATH: "/dev/ttyUSB0", + zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0"}, + zha_const.CONF_RADIO_TYPE: "ezsp", }, ) entry.add_to_hass(hass) @@ -62,19 +52,18 @@ async def config_entry_fixture(hass): @pytest.fixture -def setup_zha(hass, config_entry, zigpy_app_controller, zigpy_radio): +def setup_zha(hass, config_entry, zigpy_app_controller): """Set up ZHA component.""" zha_config = {zha_const.CONF_ENABLE_QUIRKS: False} - radio_details = { - zha_const.ZHA_GW_RADIO: mock.MagicMock(return_value=zigpy_radio), - zha_const.CONTROLLER: mock.MagicMock(return_value=zigpy_app_controller), - zha_const.ZHA_GW_RADIO_DESCRIPTION: "mock radio", - } + p1 = patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) async def _setup(config=None): config = config or {} - with mock.patch.dict(zha_regs.RADIO_TYPES, {"MockRadio": radio_details}): + with p1: status = await async_setup_component( hass, zha_const.DOMAIN, {zha_const.DOMAIN: {**zha_config, **config}} ) @@ -89,12 +78,12 @@ def channel(): """Channel mock factory fixture.""" def channel(name: str, cluster_id: int, endpoint_id: int = 1): - ch = mock.MagicMock() + ch = MagicMock() ch.name = name ch.generic_id = f"channel_0x{cluster_id:04x}" ch.id = f"{endpoint_id}:0x{cluster_id:04x}" - ch.async_configure = asynctest.CoroutineMock() - ch.async_initialize = asynctest.CoroutineMock() + ch.async_configure = AsyncMock() + ch.async_initialize = AsyncMock() return ch return channel @@ -198,7 +187,7 @@ def zha_device_mock(hass, zigpy_device_mock): zigpy_device = zigpy_device_mock( endpoints, ieee, manufacturer, model, node_desc ) - zha_device = zha_core_device.ZHADevice(hass, zigpy_device, mock.MagicMock()) + zha_device = zha_core_device.ZHADevice(hass, zigpy_device, MagicMock()) return zha_device return _zha_device diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index b67a39cd3ab..88fd1e8437f 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -244,21 +244,26 @@ async def test_list_groupable_devices(zha_client, device_groupable): assert msg["id"] == 10 assert msg["type"] == const.TYPE_RESULT - devices = msg["result"] - assert len(devices) == 1 + device_endpoints = msg["result"] + assert len(device_endpoints) == 1 - for device in devices: - assert device[ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" - assert device[ATTR_MANUFACTURER] is not None - assert device[ATTR_MODEL] is not None - assert device[ATTR_NAME] is not None - assert device[ATTR_QUIRK_APPLIED] is not None - assert device["entities"] is not None + for endpoint in device_endpoints: + assert endpoint["device"][ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" + assert endpoint["device"][ATTR_MANUFACTURER] is not None + assert endpoint["device"][ATTR_MODEL] is not None + assert endpoint["device"][ATTR_NAME] is not None + assert endpoint["device"][ATTR_QUIRK_APPLIED] is not None + assert endpoint["device"]["entities"] is not None + assert endpoint["endpoint_id"] is not None + assert endpoint["entities"] is not None - for entity_reference in device["entities"]: + for entity_reference in endpoint["device"]["entities"]: assert entity_reference[ATTR_NAME] is not None assert entity_reference["entity_id"] is not None + for entity_reference in endpoint["entities"]: + assert entity_reference["original_name"] is not None + # Make sure there are no groupable devices when the device is unavailable # Make device unavailable device_groupable.set_available(False) @@ -269,8 +274,8 @@ async def test_list_groupable_devices(zha_client, device_groupable): assert msg["id"] == 11 assert msg["type"] == const.TYPE_RESULT - devices = msg["result"] - assert len(devices) == 0 + device_endpoints = msg["result"] + assert len(device_endpoints) == 0 async def test_add_group(zha_client): diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 1196cdc3b40..471e44f8409 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -2,7 +2,6 @@ import asyncio from unittest import mock -import asynctest import pytest import zigpy.types as t import zigpy.zcl.clusters @@ -14,6 +13,8 @@ import homeassistant.components.zha.core.registries as registries from .common import get_zha_gateway, make_zcl_header +import tests.async_mock + @pytest.fixture def ieee(): @@ -379,12 +380,12 @@ async def test_ep_channels_configure(channel): ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_3 = channel(zha_const.CHANNEL_COLOR, 768) - ch_3.async_configure = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) - ch_3.async_initialize = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) + ch_3.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) ch_4 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_5 = channel(zha_const.CHANNEL_LEVEL, 8) - ch_5.async_configure = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) - ch_5.async_initialize = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) + ch_5.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) channels = mock.MagicMock(spec_set=zha_channels.Channels) type(channels).semaphore = mock.PropertyMock(return_value=asyncio.Semaphore(3)) @@ -420,8 +421,8 @@ async def test_poll_control_configure(poll_control_ch): async def test_poll_control_checkin_response(poll_control_ch): """Test poll control channel checkin response.""" - rsp_mock = asynctest.CoroutineMock() - set_interval_mock = asynctest.CoroutineMock() + rsp_mock = tests.async_mock.AsyncMock() + set_interval_mock = tests.async_mock.AsyncMock() cluster = poll_control_ch.cluster patch_1 = mock.patch.object(cluster, "checkin_response", rsp_mock) patch_2 = mock.patch.object(cluster, "set_long_poll_interval", set_interval_mock) @@ -442,7 +443,7 @@ async def test_poll_control_checkin_response(poll_control_ch): async def test_poll_control_cluster_command(hass, poll_control_device): """Test poll control channel response to cluster command.""" - checkin_mock = asynctest.CoroutineMock() + checkin_mock = tests.async_mock.AsyncMock() poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] cluster = poll_control_ch.cluster diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 7e0b89f8d70..9894360da46 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,126 +1,244 @@ """Tests for ZHA config flow.""" -from unittest import mock -import asynctest +import os +import pytest +import serial.tools.list_ports +import zigpy.config + +from homeassistant import setup from homeassistant.components.zha import config_flow -from homeassistant.components.zha.core.const import CONTROLLER, DOMAIN, ZHA_GW_RADIO -import homeassistant.components.zha.core.registries +from homeassistant.components.zha.core.const import CONF_RADIO_TYPE, DOMAIN, RadioType +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_SOURCE +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from tests.async_mock import AsyncMock, MagicMock, patch, sentinel from tests.common import MockConfigEntry -async def test_user_flow(hass): - """Test that config flow works.""" - flow = config_flow.ZhaFlowHandler() - flow.hass = hass +def com_port(): + """Mock of a serial port.""" + port = serial.tools.list_ports_common.ListPortInfo() + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/dev/ttyUSB1234" + port.description = "Some serial port" - with asynctest.patch( - "homeassistant.components.zha.config_flow.check_zigpy_connection", - return_value=False, - ): - result = await flow.async_step_user( - user_input={"usb_path": "/dev/ttyUSB1", "radio_type": "ezsp"} - ) + return port - assert result["errors"] == {"base": "cannot_connect"} - with asynctest.patch( - "homeassistant.components.zha.config_flow.check_zigpy_connection", - return_value=True, - ): - result = await flow.async_step_user( - user_input={"usb_path": "/dev/ttyUSB1", "radio_type": "ezsp"} - ) +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +@patch( + "homeassistant.components.zha.config_flow.detect_radios", + return_value={CONF_RADIO_TYPE: "test_radio"}, +) +async def test_user_flow(detect_mock, hass): + """Test user flow -- radio detected.""" - assert result["type"] == "create_entry" - assert result["title"] == "/dev/ttyUSB1" - assert result["data"] == {"usb_path": "/dev/ttyUSB1", "radio_type": "ezsp"} + port = com_port() + port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={zigpy.config.CONF_DEVICE_PATH: port_select}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"].startswith(port.description) + assert result["data"] == {CONF_RADIO_TYPE: "test_radio"} + assert detect_mock.await_count == 1 + assert detect_mock.await_args[0][0] == port.device + + +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +@patch( + "homeassistant.components.zha.config_flow.detect_radios", return_value=None, +) +async def test_user_flow_not_detected(detect_mock, hass): + """Test user flow, radio not detected.""" + + port = com_port() + port_select = f"{port}, s/n: {port.serial_number} - {port.manufacturer}" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={zigpy.config.CONF_DEVICE_PATH: port_select}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pick_radio" + assert detect_mock.await_count == 1 + assert detect_mock.await_args[0][0] == port.device + + +async def test_user_flow_show_form(hass): + """Test user step form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_user_flow_manual(hass): + """Test user flow manual entry.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pick_radio" + + +@pytest.mark.parametrize("radio_type", RadioType.list()) +async def test_pick_radio_flow(hass, radio_type): + """Test radio picker.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: radio_type} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "port_config" async def test_user_flow_existing_config_entry(hass): """Test if config entry already exists.""" MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass) - flow = config_flow.ZhaFlowHandler() - flow.hass = hass - - result = await flow.async_step_user() + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) assert result["type"] == "abort" -async def test_import_flow(hass): - """Test import from configuration.yaml .""" - flow = config_flow.ZhaFlowHandler() - flow.hass = hass +@patch("zigpy_cc.zigbee.application.ControllerApplication.probe", return_value=False) +@patch( + "zigpy_deconz.zigbee.application.ControllerApplication.probe", return_value=False +) +@patch( + "zigpy_zigate.zigbee.application.ControllerApplication.probe", return_value=False +) +@patch("zigpy_xbee.zigbee.application.ControllerApplication.probe", return_value=False) +async def test_probe_radios(xbee_probe, zigate_probe, deconz_probe, cc_probe, hass): + """Test detect radios.""" + app_ctrl_cls = MagicMock() + app_ctrl_cls.SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE + app_ctrl_cls.probe = AsyncMock(side_effect=(True, False)) - result = await flow.async_step_import( - {"usb_path": "/dev/ttyUSB1", "radio_type": "xbee"} + p1 = patch( + "bellows.zigbee.application.ControllerApplication.probe", + side_effect=(True, False), + ) + with p1 as probe_mock: + res = await config_flow.detect_radios("/dev/null") + assert probe_mock.await_count == 1 + assert res[CONF_RADIO_TYPE] == "ezsp" + assert zigpy.config.CONF_DEVICE in res + assert ( + res[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] == "/dev/null" + ) + + res = await config_flow.detect_radios("/dev/null") + assert res is None + assert xbee_probe.await_count == 1 + assert zigate_probe.await_count == 1 + assert deconz_probe.await_count == 1 + assert cc_probe.await_count == 1 + + +@patch("bellows.zigbee.application.ControllerApplication.probe", return_value=False) +async def test_user_port_config_fail(probe_mock, hass): + """Test port config flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: "pick_radio"}, + data={CONF_RADIO_TYPE: RadioType.ezsp.description}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "port_config" + assert result["errors"]["base"] == "cannot_connect" + assert probe_mock.await_count == 1 + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +@patch("bellows.zigbee.application.ControllerApplication.probe", return_value=True) +async def test_user_port_config(probe_mock, hass): + """Test port config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: "pick_radio"}, + data={CONF_RADIO_TYPE: RadioType.ezsp.description}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) assert result["type"] == "create_entry" - assert result["title"] == "/dev/ttyUSB1" - assert result["data"] == {"usb_path": "/dev/ttyUSB1", "radio_type": "xbee"} - - -async def test_import_flow_existing_config_entry(hass): - """Test import from configuration.yaml .""" - MockConfigEntry(domain=DOMAIN, data={"usb_path": "/dev/ttyUSB1"}).add_to_hass(hass) - flow = config_flow.ZhaFlowHandler() - flow.hass = hass - - result = await flow.async_step_import( - {"usb_path": "/dev/ttyUSB1", "radio_type": "xbee"} + assert result["title"].startswith("/dev/ttyUSB33") + assert ( + result["data"][zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] + == "/dev/ttyUSB33" ) - - assert result["type"] == "abort" + assert result["data"][CONF_RADIO_TYPE] == "ezsp" + assert probe_mock.await_count == 1 -async def test_check_zigpy_connection(): - """Test config flow validator.""" +def test_get_serial_by_id_no_dir(): + """Test serial by id conversion if there's no /dev/serial/by-id.""" + p1 = patch("os.path.isdir", MagicMock(return_value=False)) + p2 = patch("os.scandir") + with p1 as is_dir_mock, p2 as scan_mock: + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.path + assert is_dir_mock.call_count == 1 + assert scan_mock.call_count == 0 - mock_radio = asynctest.MagicMock() - mock_radio.connect = asynctest.CoroutineMock() - radio_cls = asynctest.MagicMock(return_value=mock_radio) - bad_radio = asynctest.MagicMock() - bad_radio.connect = asynctest.CoroutineMock(side_effect=Exception) - bad_radio_cls = asynctest.MagicMock(return_value=bad_radio) +def test_get_serial_by_id(): + """Test serial by id conversion.""" + p1 = patch("os.path.isdir", MagicMock(return_value=True)) + p2 = patch("os.scandir") - mock_ctrl = asynctest.MagicMock() - mock_ctrl.startup = asynctest.CoroutineMock() - mock_ctrl.shutdown = asynctest.CoroutineMock() - ctrl_cls = asynctest.MagicMock(return_value=mock_ctrl) - new_radios = { - mock.sentinel.radio: {ZHA_GW_RADIO: radio_cls, CONTROLLER: ctrl_cls}, - mock.sentinel.bad_radio: {ZHA_GW_RADIO: bad_radio_cls, CONTROLLER: ctrl_cls}, - } + def _realpath(path): + if path is sentinel.matched_link: + return sentinel.path + return sentinel.serial_link_path - with mock.patch.dict( - homeassistant.components.zha.core.registries.RADIO_TYPES, new_radios, clear=True - ): - assert not await config_flow.check_zigpy_connection( - mock.sentinel.usb_path, mock.sentinel.unk_radio, mock.sentinel.zigbee_db - ) - assert mock_radio.connect.call_count == 0 - assert bad_radio.connect.call_count == 0 - assert mock_ctrl.startup.call_count == 0 - assert mock_ctrl.shutdown.call_count == 0 + p3 = patch("os.path.realpath", side_effect=_realpath) + with p1 as is_dir_mock, p2 as scan_mock, p3: + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.path + assert is_dir_mock.call_count == 1 + assert scan_mock.call_count == 1 - # unsuccessful radio connect - assert not await config_flow.check_zigpy_connection( - mock.sentinel.usb_path, mock.sentinel.bad_radio, mock.sentinel.zigbee_db - ) - assert mock_radio.connect.call_count == 0 - assert bad_radio.connect.call_count == 1 - assert mock_ctrl.startup.call_count == 0 - assert mock_ctrl.shutdown.call_count == 0 + entry1 = MagicMock(spec_set=os.DirEntry) + entry1.is_symlink.return_value = True + entry1.path = sentinel.some_path - # successful radio connect - assert await config_flow.check_zigpy_connection( - mock.sentinel.usb_path, mock.sentinel.radio, mock.sentinel.zigbee_db - ) - assert mock_radio.connect.call_count == 1 - assert bad_radio.connect.call_count == 1 - assert mock_ctrl.startup.call_count == 1 - assert mock_ctrl.shutdown.call_count == 1 + entry2 = MagicMock(spec_set=os.DirEntry) + entry2.is_symlink.return_value = False + entry2.path = sentinel.other_path + + entry3 = MagicMock(spec_set=os.DirEntry) + entry3.is_symlink.return_value = True + entry3.path = sentinel.matched_link + + scan_mock.return_value = [entry1, entry2, entry3] + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.matched_link + assert is_dir_mock.call_count == 2 + assert scan_mock.call_count == 2 diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 188ddf69a23..b4c72fd82d4 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,7 +1,4 @@ """Test zha cover.""" -from unittest.mock import MagicMock, call, patch - -import asynctest import pytest import zigpy.types import zigpy.zcl.clusters.closures as closures @@ -17,6 +14,7 @@ from .common import ( send_attributes_report, ) +from tests.async_mock import MagicMock, call, patch from tests.common import mock_coro @@ -34,7 +32,7 @@ def zigpy_cover_device(zigpy_device_mock): return zigpy_device_mock(endpoints) -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" ) async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index c92f574825d..6e528299911 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -3,7 +3,6 @@ from datetime import timedelta import time from unittest import mock -import asynctest import pytest import zigpy.zcl.clusters.general as general @@ -13,6 +12,7 @@ import homeassistant.util.dt as dt_util from .common import async_enable_traffic, make_zcl_header +from tests.async_mock import patch from tests.common import async_fire_time_changed @@ -90,7 +90,7 @@ def _send_time_changed(hass, seconds): async_fire_time_changed(hass, now) -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", new=mock.MagicMock(), ) @@ -144,7 +144,7 @@ async def test_check_available_success( assert zha_device.available is True -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", new=mock.MagicMock(), ) @@ -187,7 +187,7 @@ async def test_check_available_unsuccessful( assert zha_device.available is False -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", new=mock.MagicMock(), ) diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 8b1ce502a78..4b95040dd08 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -3,7 +3,6 @@ import re from unittest import mock -import asynctest import pytest import zigpy.quirks import zigpy.types @@ -30,6 +29,8 @@ import homeassistant.helpers.entity_registry from .common import get_zha_gateway from .zha_devices_list import DEVICES +from tests.async_mock import AsyncMock, patch + NO_TAIL_ID = re.compile("_\\d$") @@ -51,11 +52,9 @@ def channels_mock(zha_device_mock): return _mock -@asynctest.patch( +@patch( "zigpy.zcl.clusters.general.Identify.request", - new=asynctest.CoroutineMock( - return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS] - ), + new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]), ) @pytest.mark.parametrize("device", DEVICES) async def test_devices( diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 399982df37a..91819e6f457 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,6 +1,4 @@ """Test zha fan.""" -from unittest.mock import call - import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general @@ -35,6 +33,8 @@ from .common import ( send_attributes_report, ) +from tests.async_mock import call + IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @@ -62,6 +62,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index c5ae9142ff0..379e4d56492 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,6 +1,8 @@ """Test ZHA Gateway.""" +import asyncio import logging import time +from unittest.mock import patch import pytest import zigpy.profiles.zha as zha @@ -8,6 +10,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.zha.core.group import GroupMember from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway @@ -52,6 +55,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -127,17 +131,16 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord device_light_1._zha_gateway = zha_gateway device_light_2._zha_gateway = zha_gateway member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 2 for member in zha_group.members: - assert member.ieee in member_ieee_addresses + assert member.device.ieee in member_ieee_addresses entity_id = async_find_group_entity_id(hass, LIGHT_DOMAIN, zha_group) assert hass.states.get(entity_id) is not None @@ -157,18 +160,24 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord # test creating a group with 1 member zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", [device_light_1.ieee] + "Test Group", [GroupMember(device_light_1.ieee, 1)] ) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 1 for member in zha_group.members: - assert member.ieee in [device_light_1.ieee] + assert member.device.ieee in [device_light_1.ieee] # the group entity should not have been cleaned up assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + await zha_group.members[0].async_remove_from_group() + assert len(zha_group.members) == 1 + for member in zha_group.members: + assert member.device.ieee in [device_light_1.ieee] + async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): """Test saving data after a delay.""" diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py new file mode 100644 index 00000000000..f0b82231fa9 --- /dev/null +++ b/tests/components/zha/test_init.py @@ -0,0 +1,97 @@ +"""Tests for ZHA integration init.""" + +import pytest +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH + +from homeassistant.components.zha.core.const import ( + CONF_BAUDRATE, + CONF_RADIO_TYPE, + CONF_USB_PATH, + DOMAIN, +) +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION +from homeassistant.setup import async_setup_component + +from tests.async_mock import AsyncMock, patch +from tests.common import MockConfigEntry + +DATA_RADIO_TYPE = "deconz" +DATA_PORT_PATH = "/dev/serial/by-id/FTDI_USB__-__Serial_Cable_12345678-if00-port0" + + +@pytest.fixture +def config_entry_v1(hass): + """Config entry version 1 fixture.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_RADIO_TYPE: DATA_RADIO_TYPE, CONF_USB_PATH: DATA_PORT_PATH}, + version=1, + ) + + +@pytest.mark.parametrize("config", ({}, {DOMAIN: {}})) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migration_from_v1_no_baudrate(hass, config_entry_v1, config): + """Test migration of config entry from v1.""" + config_entry_v1.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + + assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE + assert CONF_DEVICE in config_entry_v1.data + assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH + assert CONF_BAUDRATE not in config_entry_v1.data[CONF_DEVICE] + assert CONF_USB_PATH not in config_entry_v1.data + assert config_entry_v1.version == 2 + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migration_from_v1_with_baudrate(hass, config_entry_v1): + """Test migration of config entry from v1 with baudrate in config.""" + config_entry_v1.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_BAUDRATE: 115200}}) + + assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE + assert CONF_DEVICE in config_entry_v1.data + assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH + assert CONF_USB_PATH not in config_entry_v1.data + assert CONF_BAUDRATE in config_entry_v1.data[CONF_DEVICE] + assert config_entry_v1.data[CONF_DEVICE][CONF_BAUDRATE] == 115200 + assert config_entry_v1.version == 2 + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migration_from_v1_wrong_baudrate(hass, config_entry_v1): + """Test migration of config entry from v1 with wrong baudrate.""" + config_entry_v1.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_BAUDRATE: 115222}}) + + assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE + assert CONF_DEVICE in config_entry_v1.data + assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH + assert CONF_USB_PATH not in config_entry_v1.data + assert CONF_BAUDRATE not in config_entry_v1.data[CONF_DEVICE] + assert config_entry_v1.version == 2 + + +@pytest.mark.skipif( + MAJOR_VERSION != 0 or (MAJOR_VERSION == 0 and MINOR_VERSION >= 112), + reason="Not applicaable for this version", +) +@pytest.mark.parametrize( + "zha_config", + ( + {}, + {CONF_USB_PATH: "str"}, + {CONF_RADIO_TYPE: "ezsp"}, + {CONF_RADIO_TYPE: "ezsp", CONF_USB_PATH: "str"}, + ), +) +async def test_config_depreciation(hass, zha_config): + """Test config option depreciation.""" + await async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.zha.async_setup", return_value=True + ) as setup_mock: + assert await async_setup_component(hass, DOMAIN, {DOMAIN: zha_config}) + assert setup_mock.call_count == 1 diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 03c29283c20..09c6d97808c 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,8 +1,6 @@ """Test zha light.""" from datetime import timedelta -from unittest.mock import MagicMock, call, sentinel -from asynctest import CoroutineMock, patch import pytest import zigpy.profiles.zha as zha import zigpy.types @@ -11,6 +9,7 @@ import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import DOMAIN, FLASH_LONG, FLASH_SHORT +from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util @@ -24,13 +23,14 @@ from .common import ( send_attributes_report, ) +from tests.async_mock import AsyncMock, MagicMock, call, patch, sentinel from tests.common import async_fire_time_changed ON = 1 OFF = 0 IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" -IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" -IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9" +IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7" LIGHT_ON_OFF = { 1: { @@ -78,13 +78,14 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [], + "in_clusters": [general.Groups.cluster_id], "out_clusters": [], "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, } }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -110,6 +111,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE, + nwk=0xB79D, ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -135,6 +137,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, + nwk=0xC79E, ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -160,6 +163,7 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE3, + nwk=0xB89F, ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -206,19 +210,19 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored @patch( "zigpy.zcl.clusters.lighting.Color.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "zigpy.zcl.clusters.general.Identify.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "zigpy.zcl.clusters.general.LevelControl.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "zigpy.zcl.clusters.general.OnOff.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @pytest.mark.parametrize( "device, reporting", @@ -290,10 +294,12 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3}) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3}) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -301,6 +307,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2}) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -411,6 +418,7 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected await send_attributes_report( hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} ) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: @@ -439,7 +447,23 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): ) -async def async_test_zha_group_light_entity( +@patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.Identify.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_zha_group_light_entity( hass, device_light_1, device_light_2, device_light_3, coordinator ): """Test the light entity for a ZHA group.""" @@ -450,119 +474,180 @@ async def async_test_zha_group_light_entity( device_light_1._zha_gateway = zha_gateway device_light_2._zha_gateway = zha_gateway member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] + + assert coordinator.is_coordinator # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 2 for member in zha_group.members: - assert member.ieee in member_ieee_addresses + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None - entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) - assert hass.states.get(entity_id) is not None + device_1_entity_id = await find_entity_id(DOMAIN, device_light_1, hass) + device_2_entity_id = await find_entity_id(DOMAIN, device_light_2, hass) + device_3_entity_id = await find_entity_id(DOMAIN, device_light_3, hass) + + assert ( + device_1_entity_id != device_2_entity_id + and device_1_entity_id != device_3_entity_id + ) + assert device_2_entity_id != device_3_entity_id + + group_entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + assert hass.states.get(group_entity_id) is not None + + assert device_1_entity_id in zha_group.member_entity_ids + assert device_2_entity_id in zha_group.member_entity_ids + assert device_3_entity_id not in zha_group.member_entity_ids group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] group_cluster_level = zha_group.endpoint[general.LevelControl.cluster_id] group_cluster_identify = zha_group.endpoint[general.Identify.cluster_id] - dev1_cluster_on_off = device_light_1.endpoints[1].on_off - dev2_cluster_on_off = device_light_2.endpoints[1].on_off - dev3_cluster_on_off = device_light_3.endpoints[1].on_off + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off + dev3_cluster_on_off = device_light_3.device.endpoints[1].on_off + + dev1_cluster_level = device_light_1.device.endpoints[1].level # test that the lights were created and that they are unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_group.members) + await async_enable_traffic(hass, [device_light_1, device_light_2, device_light_3]) + await hass.async_block_till_done() # test that the lights were created and are off - assert hass.states.get(entity_id).state == STATE_OFF - - # test turning the lights on and off from the light - await async_test_on_off_from_light(hass, group_cluster_on_off, entity_id) + assert hass.states.get(group_entity_id).state == STATE_OFF # test turning the lights on and off from the HA - await async_test_on_off_from_hass(hass, group_cluster_on_off, entity_id) + await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) # test short flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, entity_id, FLASH_SHORT + hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) + # test turning the lights on and off from the light + await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) + # test turning the lights on and off from the HA await async_test_level_on_off_from_hass( - hass, group_cluster_on_off, group_cluster_level, entity_id + hass, group_cluster_on_off, group_cluster_level, group_entity_id ) # test getting a brightness change from the network - await async_test_on_from_light(hass, group_cluster_on_off, entity_id) + await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_dimmer_from_light( - hass, group_cluster_level, entity_id, 150, STATE_ON + hass, dev1_cluster_level, group_entity_id, 150, STATE_ON ) # test long flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, entity_id, FLASH_LONG + hass, group_cluster_identify, group_entity_id, FLASH_LONG ) + assert len(zha_group.members) == 2 # test some of the group logic to make sure we key off states correctly - await dev1_cluster_on_off.on() - await dev2_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is on - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_ON + assert hass.states.get(device_2_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON - await dev1_cluster_on_off.off() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is still on - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON - await dev2_cluster_on_off.off() + await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is now off - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_entity_id).state == STATE_OFF - await dev1_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is now back on - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_ON + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_entity_id).state == STATE_ON - # test that group light is now off - await group_cluster_on_off.off() - assert hass.states.get(entity_id).state == STATE_OFF + # turn it off to test a new member add being tracked + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await hass.async_block_till_done() + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_entity_id).state == STATE_OFF # add a new member and test that his state is also tracked - await zha_group.async_add_members([device_light_3.ieee]) - await dev3_cluster_on_off.on() - assert hass.states.get(entity_id).state == STATE_ON + await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) + await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) + await hass.async_block_till_done() + assert device_3_entity_id in zha_group.member_entity_ids + assert len(zha_group.members) == 3 + + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(device_3_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON # make the group have only 1 member and now there should be no entity - await zha_group.async_remove_members([device_light_2.ieee, device_light_3.ieee]) + await zha_group.async_remove_members( + [GroupMember(device_light_2.ieee, 1), GroupMember(device_light_3.ieee, 1)] + ) assert len(zha_group.members) == 1 - assert hass.states.get(entity_id).state is None + assert hass.states.get(group_entity_id) is None + assert device_2_entity_id not in zha_group.member_entity_ids + assert device_3_entity_id not in zha_group.member_entity_ids + # make sure the entity registry entry is still there - assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None # add a member back and ensure that the group entity was created again - await zha_group.async_add_members([device_light_3.ieee]) - await dev3_cluster_on_off.on() - assert hass.states.get(entity_id).state == STATE_ON + await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) + await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) + await hass.async_block_till_done() + assert len(zha_group.members) == 2 + assert hass.states.get(group_entity_id).state == STATE_ON # add a 3rd member and ensure we still have an entity and we track the new one - await dev1_cluster_on_off.off() - await dev3_cluster_on_off.off() - assert hass.states.get(entity_id).state == STATE_OFF + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await send_attributes_report(hass, dev3_cluster_on_off, {0: 0}) + await hass.async_block_till_done() + assert hass.states.get(group_entity_id).state == STATE_OFF + # this will test that _reprobe_group is used correctly - await zha_group.async_add_members([device_light_2.ieee]) - await dev2_cluster_on_off.on() - assert hass.states.get(entity_id).state == STATE_ON + await zha_group.async_add_members( + [GroupMember(device_light_2.ieee, 1), GroupMember(coordinator.ieee, 1)] + ) + await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) + await hass.async_block_till_done() + assert len(zha_group.members) == 4 + assert hass.states.get(group_entity_id).state == STATE_ON + + await zha_group.async_remove_members([GroupMember(coordinator.ieee, 1)]) + await hass.async_block_till_done() + assert hass.states.get(group_entity_id).state == STATE_ON + assert len(zha_group.members) == 3 # remove the group and ensure that there is no entity and that the entity registry is cleaned up - assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None await zha_gateway.async_remove_zigpy_group(zha_group.group_id) - assert hass.states.get(entity_id).state is None - assert zha_gateway.ha_entity_registry.async_get(entity_id) is None + assert hass.states.get(group_entity_id) is None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index ed5d228ab88..7bdf2ccc4d2 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -53,6 +53,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index bf92a6aa12a..994bd5e6dda 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,5 +1,4 @@ """Test zone component.""" -from asynctest import patch import pytest from homeassistant import setup @@ -16,6 +15,7 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py index f80c55f7767..50edcfec157 100644 --- a/tests/components/zwave/conftest.py +++ b/tests/components/zwave/conftest.py @@ -1,8 +1,7 @@ """Fixtures for Z-Wave tests.""" -from unittest.mock import MagicMock, patch - import pytest +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockNetwork, MockOption diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index 54270cdc3f4..8ac6370ad35 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -1,9 +1,9 @@ """Test Z-Wave binary sensors.""" import datetime -from unittest.mock import patch from homeassistant.components.zwave import binary_sensor, const +from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index e8b784feefe..cde0957e2b3 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,6 +1,4 @@ """Test Z-Wave cover devices.""" -from unittest.mock import MagicMock - from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( CONF_INVERT_OPENCLOSE_BUTTONS, @@ -9,6 +7,7 @@ from homeassistant.components.zwave import ( cover, ) +from tests.async_mock import MagicMock from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 71f7ea17f76..19733b045dc 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -3,7 +3,6 @@ import asyncio from collections import OrderedDict from datetime import datetime import unittest -from unittest.mock import MagicMock, patch import pytest from pytz import utc @@ -23,16 +22,16 @@ from homeassistant.helpers.device_registry import async_get_registry as get_dev_ from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.setup import setup_component -from tests.common import ( - async_fire_time_changed, - get_test_home_assistant, - mock_coro, - mock_registry, - mock_storage, -) +from tests.async_mock import AsyncMock, MagicMock, patch +from tests.common import async_fire_time_changed, get_test_home_assistant, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue +@pytest.fixture(autouse=True) +def mock_storage(hass_storage): + """Autouse hass_storage for the TestCase tests.""" + + async def test_valid_device_config(hass, mock_openzwave): """Test valid device config.""" device_config = {"light.kitchen": {"ignored": "true"}} @@ -829,8 +828,6 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() self.hass.start() self.registry = mock_registry(self.hass) @@ -866,13 +863,12 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - self.mock_storage.__exit__(None, None, None) @patch.object(zwave, "import_module") @patch.object(zwave, "discovery") def test_entity_discovery(self, discovery, import_module): """Test the creation of a new entity.""" - discovery.async_load_platform.return_value = mock_coro() + discovery.async_load_platform = AsyncMock(return_value=None) mock_platform = MagicMock() import_module.return_value = mock_platform mock_device = MagicMock() @@ -934,7 +930,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, "discovery") def test_entity_existing_values(self, discovery, import_module): """Test the loading of already discovered values.""" - discovery.async_load_platform.return_value = mock_coro() + discovery.async_load_platform = AsyncMock(return_value=None) mock_platform = MagicMock() import_module.return_value = mock_platform mock_device = MagicMock() @@ -1002,7 +998,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, "discovery") def test_entity_workaround_component(self, discovery, import_module): """Test component workaround.""" - discovery.async_load_platform.return_value = mock_coro() + discovery.async_load_platform = AsyncMock(return_value=None) mock_platform = MagicMock() import_module.return_value = mock_platform mock_device = MagicMock() @@ -1199,8 +1195,6 @@ class TestZWaveServices(unittest.TestCase): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.mock_storage = mock_storage() - self.mock_storage.__enter__() self.hass.start() # Initialize zwave @@ -1216,7 +1210,6 @@ class TestZWaveServices(unittest.TestCase): self.hass.services.call("zwave", "stop_network", {}) self.hass.block_till_done() self.hass.stop() - self.mock_storage.__exit__(None, None, None) def test_add_node(self): """Test zwave add_node service.""" diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index fc62ef880f6..1b973294daf 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,6 +1,4 @@ """Test Z-Wave lights.""" -from unittest.mock import MagicMock, patch - from homeassistant.components import zwave from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,6 +14,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.zwave import const, light +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -234,7 +233,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert not device.is_on - with patch.object(light, "Timer", MagicMock()) as mock_timer: + with patch.object(light, "Timer") as mock_timer: value.data = 46 value_changed(value) @@ -246,7 +245,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert mock_timer().start.called assert len(mock_timer().start.mock_calls) == 1 - with patch.object(light, "Timer", MagicMock()) as mock_timer_2: + with patch.object(light, "Timer") as mock_timer_2: value_changed(value) assert not device.is_on assert mock_timer().cancel.called diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index d5b6d0a0d27..2f82bcb2764 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,9 +1,8 @@ """Test Z-Wave locks.""" -from unittest.mock import MagicMock, patch - from homeassistant import config_entries from homeassistant.components.zwave import const, lock +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 4d117179328..8306899ce02 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,12 +1,12 @@ """Test Z-Wave node entity.""" import unittest -from unittest.mock import MagicMock, patch import pytest from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID +from tests.async_mock import MagicMock, patch import tests.mock.zwave as mock_zwave diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index 4293a4a23fd..b61c456ccb9 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -1,8 +1,7 @@ """Test Z-Wave switches.""" -from unittest.mock import patch - from homeassistant.components.zwave import switch +from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/conftest.py b/tests/conftest.py index 700240bf627..efaf1ff7dff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ """Set up some common test helper things.""" import functools import logging -from unittest.mock import patch import pytest import requests_mock as _requests_mock @@ -19,6 +18,9 @@ from homeassistant.exceptions import ServiceNotFound from homeassistant.setup import async_setup_component from homeassistant.util import location +from tests.async_mock import patch +from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS + pytest.register_assert_rewrite("tests.common") from tests.common import ( # noqa: E402, isort:skip @@ -26,7 +28,6 @@ from tests.common import ( # noqa: E402, isort:skip INSTANCES, MockUser, async_test_home_assistant, - mock_coro, mock_storage as mock_storage, ) from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip @@ -36,6 +37,13 @@ logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) +def pytest_configure(config): + """Register marker for tests that log exceptions.""" + config.addinivalue_line( + "markers", "no_fail_on_log_exception: mark test to not fail on logged exception" + ) + + def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" @@ -95,6 +103,11 @@ def hass(loop, hass_storage, request): loop.run_until_complete(hass.async_stop(force=True)) for ex in exceptions: + if ( + request.module.__name__, + request.function.__name__, + ) in IGNORE_UNCAUGHT_EXCEPTIONS: + continue if isinstance(ex, ServiceNotFound): continue raise ex @@ -128,7 +141,7 @@ def mock_device_tracker_conf(): side_effect=mock_update_config, ), patch( "homeassistant.components.device_tracker.legacy.async_load_config", - side_effect=lambda *args: mock_coro(devices), + side_effect=lambda *args: devices, ): yield devices @@ -242,3 +255,15 @@ def hass_ws_client(aiohttp_client, hass_access_token, hass): return websocket return create_client + + +@pytest.fixture(autouse=True) +def fail_on_log_exception(request, monkeypatch): + """Fixture to fail if a callback wrapped by catch_log_exception or coroutine wrapped by async_create_catching_coro throws.""" + if "no_fail_on_log_exception" in request.keywords: + return + + def log_exception(format_err, *args): + raise + + monkeypatch.setattr("homeassistant.util.logging.log_exception", log_exception) diff --git a/tests/fixtures/agent_dvr/objects.json b/tests/fixtures/agent_dvr/objects.json new file mode 100644 index 00000000000..883679b47cb --- /dev/null +++ b/tests/fixtures/agent_dvr/objects.json @@ -0,0 +1 @@ +{"settings":{"canUpdate":true,"supportsPlugins":true,"isArmed":true,"background":"255,255,255"},"directories":[{"ID":0,"dir":"D:\\Projects\\agent-service\\AgentService\\Media\\WebServerRoot\\Media\\"}],"locations":[],"objectList": [],"profiles": [{"name":"Home","active":true,"id":0},{"name":"Away","active":false,"id":1},{"name":"Night","active":false,"id":2}],"views":[{"name":"0","mode":"column","objects":[],"maxWidth":1266,"maxHeight":1222,"backColor":"#222222","id":1,"typeID":2,"focused":false},{"name":"1","mode":"grid","objects":[]},{"name":"2","mode":"grid","objects":[]},{"name":"3","mode":"grid","objects":[]},{"name":"4","mode":"grid","objects":[]},{"name":"5","mode":"grid","objects":[]},{"name":"6","mode":"grid","objects":[]},{"name":"7","mode":"grid","objects":[]},{"name":"8","mode":"grid","objects":[]}],"rtmpStreaming":false} \ No newline at end of file diff --git a/tests/fixtures/agent_dvr/status.json b/tests/fixtures/agent_dvr/status.json new file mode 100644 index 00000000000..20c77b16bab --- /dev/null +++ b/tests/fixtures/agent_dvr/status.json @@ -0,0 +1,10 @@ +{ + "armed": false, + "devices": 4, + "active": 4, + "recording": 0, + "remoteAccess": true, + "unique": "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447", + "name": "DESKTOP", + "version": "2.6.1.0" +} diff --git a/tests/fixtures/bsblan/info.json b/tests/fixtures/bsblan/info.json new file mode 100644 index 00000000000..82c8b919cc9 --- /dev/null +++ b/tests/fixtures/bsblan/info.json @@ -0,0 +1,23 @@ +{ + "6224": { + "name": "Geräte-Identifikation", + "value": "RVS21.831F/127", + "unit": "", + "desc": "", + "dataType": 7 + }, + "6225": { + "name": "Device family", + "value": "211", + "unit": "", + "desc": "", + "dataType": 0 + }, + "6226": { + "name": "Device variant", + "value": "127", + "unit": "", + "desc": "", + "dataType": 0 + } +} \ No newline at end of file diff --git a/tests/fixtures/hunterdouglas_powerview/userdata.json b/tests/fixtures/hunterdouglas_powerview/userdata.json new file mode 100644 index 00000000000..ca5eea73f7b --- /dev/null +++ b/tests/fixtures/hunterdouglas_powerview/userdata.json @@ -0,0 +1,50 @@ +{ + "userData": { + "_id": "abc", + "color": { + "green": 0, + "blue": 255, + "brightness": 5, + "red": 0 + }, + "autoBackup": false, + "ip": "192.168.1.72", + "macAddress": "aa:bb:cc:dd:ee:ff", + "mask": "255.255.255.0", + "gateway": "192.168.1.1", + "dns": "192.168.1.3", + "firmware": { + "mainProcessor": { + "name": "PV Hub2.0", + "revision": 2, + "subRevision": 0, + "build": 1024 + }, + "radio": { + "revision": 2, + "subRevision": 0, + "build": 2610 + } + }, + "serialNumber": "ABC123", + "rfIDInt": 64789, + "rfID": "0xFD15", + "rfStatus": 0, + "brand": "HD", + "wireless": false, + "hubName": "QWxleGFuZGVySEQ=", + "localTimeDataSet": true, + "enableScheduledEvents": true, + "editingEnabled": true, + "setupCompleted": false, + "staticIp": false, + "times": { + "timezone": "America/Chicago", + "localSunriseTimeInMinutes": 0, + "localSunsetTimeInMinutes": 0, + "currentOffset": -18000 + }, + "rcUp": true, + "remoteConnectEnabled": true + } +} diff --git a/tests/fixtures/ipp/get-printer-attributes-success-nodata.bin b/tests/fixtures/ipp/get-printer-attributes-success-nodata.bin new file mode 100644 index 00000000000..e6061adaccd Binary files /dev/null and b/tests/fixtures/ipp/get-printer-attributes-success-nodata.bin differ diff --git a/tests/fixtures/ozw/binary_sensor.json b/tests/fixtures/ozw/binary_sensor.json new file mode 100644 index 00000000000..4d6317827d1 --- /dev/null +++ b/tests/fixtures/ozw/binary_sensor.json @@ -0,0 +1,38 @@ +{ + "topic": "OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/", + "payload": { + "Label": "Home Security", + "Value": { + "List": [ + { + "Value": 0, + "Label": "Clear" + }, + { + "Value": 8, + "Label": "Motion Detected at Unknown Location" + } + ], + "Selected": "Motion Detected at Unknown Location", + "Selected_id": 8 + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_NOTIFICATION", + "Index": 7, + "Node": 37, + "Genre": "User", + "Help": "Home Security Alerts", + "ValueIDKey": 1970325463777300, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} \ No newline at end of file diff --git a/tests/fixtures/ozw/binary_sensor_alt.json b/tests/fixtures/ozw/binary_sensor_alt.json new file mode 100644 index 00000000000..187028843ff --- /dev/null +++ b/tests/fixtures/ozw/binary_sensor_alt.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/", + "payload": { + "Label": "Sensor", + "Value": true, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "Bool", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", + "Index": 0, + "Node": 37, + "Genre": "User", + "Help": "Binary Sensor State", + "ValueIDKey": 625737744, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} \ No newline at end of file diff --git a/tests/fixtures/ozw/generic_network_dump.csv b/tests/fixtures/ozw/generic_network_dump.csv new file mode 100644 index 00000000000..9214796759a --- /dev/null +++ b/tests/fixtures/ozw/generic_network_dump.csv @@ -0,0 +1,282 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/1/,{ "NodeID": 1, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": false, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:005A:0101", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw090.png", "Description": "Aeotec Z-Stick Gen5 is a USB controller. When connected to a host controller via USB, it enables the host controller to take part in the Z-Wave network. Products that are Z-Wave certified can be used and communicate with other Z-Wave certified devices.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1355/Z Stick Gen5 manual 1.pdf", "ProductPageURL": "", "InclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Inclusion” button on your PC/host Controller application.", "ExclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Exclusion” button on your PC/host Controller application.", "ResetHelp": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable. Press and hold the Action Button on Z-Stick for 20 seconds and then release.", "WakeupHelp": "N/A", "ProductSupportURL": "", "Frequency": "", "Name": "Z-Stick Gen5", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAG4AAADICAIAAACGfENfAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO19aXhcxZVo1V160Wrtthbb8iKBsORFsmzACwkMMA4JkyEkkEBwIJPwJR4eCUlMHrwM2UOACUMIIclkmzwyBBKME5YEs3jDNshgQ7AsL5IsedFuqVu997233o9Sl6pr6yu5BfN9j/Pp01f31KlT55w6dU5V3aUhQgi4A4QQhNBNLS4z9AqkmsYl/+mpIOsLAMB0l5EVdG9K9+Cybxnyfxq4FBI6jpOBAmbB3NNgMqUmboizooiCrUajiO1xgR8K4eCQhvx/GRMaSehl3QmrhMwZSdw0USglU03GVlMISo8hbRoaZKyFUsouXQog65QOam7YqqXF/0nXCkdmhDGYXt3L4T7MKbxVaBfCWZagaG1lAiv8iGEra6tgC0R+ljlWvg8uYUYi8f+fYBBTkllDUhKf8vipQZORucms+2iefEcMJS+iuorpRchZpo6Cv1B+niExxUTaYVgzUYkJOrJcxIuoAMdxEEKYFRNzhfLwJkApcJncM+LVoHCpSaRMIJcbA2GrjBsb27Z5gikB6ULXdZfEU62aKs/3Ju0kEon+/v5gIKClDMEPG9NE0zQGgxAqKCiYM2cOX/WegDFtvzgX6OzsfPPNN9/Yv980ffwiSbBwhVCDGkiXFEJYVDTr5ls+k5eX9+6KLwZB2gHpEVqRTJiGbigx0uPxFBUV5ebleUw/th4AAEAAAdS0yfBN3C1l1clxxxiv1080kWUJQiBEAi7tCFsJqxi2Gu0UGYMrYx33Hq3IwpM0QMwNcs4oiwZCEyiEFC6/3eRVYjQab8ga0CbmCaaUu2niCfXSKiHgQmE6W9JcEDF5YRTq0JfqhZHMeXlWBGOoPYUBNTFZ3DD/hQQ0csIEAPBLHiDZRwqlUojH0AiFpJtnrOK7e2/SjlBjJPEjKFkY85TvLRjChTuQhGFZIAfUpBCmHVnAJh05jkNCD28ykoscx8ELSXpZJ+OvkHl6VfRw8snNUAvBd0MrIGyVEdKdCEEINU0zTRObEiEUj8fxJZllhmHgKIkQSiaTpmkKGSoyuCLvTa+K75qN4sLJokAKU5uiP2FVaDzo9Xrwn2ka4VCIEDiOMzZ6VtOgpkFd0zQNjo+PC3dowuQzVSHppCwj43mmpR2h4Xmkm7RDlzOmHSyEz+8vLS3Tdd0wDCuZHBgYoH3fn5NbUlJqGIZpmgihwcFBrA/J5tPOnO51V1ASeG/SDgUQQhCLxUZGRkzTNE3Ttm3HtumsbSWTIyMjhmF4vV6QOshQLHTeKzBAepiXhWGCoWncBGM+bAEA4ORMQRBq48Gg4ziapmlQQwBpmobzDG4VCo07aCIpQQh1TWdsp96NMFXC3CLU1CX9ZNoBklAisy9tHaKJTAdpVErHV1ZVYyaQWq5DCAECuq7PqayiQ5imaRACujcipMKm6iyvMKiMIeAMapBuhH0wsgr7YAqy5mkEnMQ8zYRxQbpxAUgNg3hpJbtkJKFVlsUKNymU/i9IO4rNg5sAL9xIsDSikEcfZADiR3gXRMUNEhzIAExvWaZQU62UDN7ztDPhZZNipAsEJzZBqo3je68CAEC92+EzBk0GXOx2aBrJkHJmQCjduG7NJIt3VE9TvrfDyK9GajwjdUxkTK8GdcDSdT0V/QRL9/Q8AwFMQ6q7o6e/mxiakaHQJkyP0kcKZN0L465aRDc0GUF2mqnuRZElMPATziVDnjKbux2aRrHbIWU33CZFpyY+pU/mbJBR8ullLb7he5N2+C7p+SgsAx5DtX03hM4EbtMO8x9wU8MNpRpky9tUc6nFhBkSyDZayvszso2Nggmbdmih1XOBto57d+DDDWEFISTbRFlbvMNhaPgmNE/eZLJoqBg/tfvTAkykHWJ4RehlLhVqC/GAsiCLVa4ZFV3LCsyly1Gn26p3mTJWk/d21EnGTXZS0LMJR84ko868adzsW9wIydMzrdRKvRtph+wRiRXwM0MAAIAAAmmmoQtU9kbkFB3P9xQHaNsOo5ubR19mAjLfcRTGXQUl04rUktVPIpGIRCIIoVg0GoskvD6fz+czDdP0eDRN83g8hq4To0NNSyYStmMDABOJuG3blmUnk0krmYSaVlpaEg6HdV3zer30uZxMKv5SpqOCTIaExGWIwkxaIGXeNEya5jM4obQsKxwOnzlz5szpM5FoNBqJ6rpeOKuwsLAYnz+iFID0sGXbNqDSCC1SJBIOBALJZNy2bZ/Pm5uXt2DBgsrKSq/XQ0soFJIfb/UKRIZM60jmawrWMnoemUgkzp49e/z48WAgYNl2QUFhYUERywQbTsASAoAmt+L0eQdMO7DEY2Db9tDQYDgS8vu8paWldXV1efn5IP1pGUZ/SoQpOA2NpMtZfpINM00mk8FgsP3QoeD4uK7rFRUV9BpF2FC2znCTeWmnxhCPx/v6+jQI59fWLly4EN/JmOmskDkCugesRjAY3L9/fzQaraioME1T13WSNIBcH/4JlozWJ53SZewZuq5DCG3bPnv27NDQ0JIlS2prazHyXBRUw6RXug+0PJAYd/r06Xf+/veq6mpd14XPo/KZAdcytlZYkJFHvbjxeDw+n6+rqysnJ2fZsmXqgclC2mEkyxiGaUoiPUJoeHj46NGjixcvDoVCtm07jiOMHrQ+ZOHC3IklNHx0U5gScBEQQmgYRklJyZGOjvz8/Lr6eiAKJqRTWR5nqoSWMXhVCRe+V56AgOM4Rzo6mltaYrEY4HyNUY+f73yBvxSKxIwWPU5kBgSDwfMbGl7dvbuqujo3N5dxCJmapDthghKIl5W04zhO+6FDFbNnFxUV9ff3x+NxQEUuWizGZAopMaiXDQwlKeCwSAMAIBgMJhOJxqamGXrgOju7HQhhNBotKCiIRqPJZJIgCXNaepJweY8TziZmymNQLE1AaneE3R93bVlWQUFBR0cHeYAr65D2SIEww/ARhCcIBAKFs2bpuh4Oh2kyTdOYGUTW4UCUMegy3516ogndHKaOnfAU0TXNsizDkIY1Rms1krGMISSVmU+WdpLJpNfjAZKsRdMDiQNm1I3pnW/ChDZiR/zwDC4TIzK5BVBjQCYNXaXwM1Jgn85QhFWFYvhaqJgM4ybPuOmXNwr5j20nY8hbkL9005CA6jjDzaQTIoWO5tL7hCs7JikzTRjHJ6bUdR1vEOioIlONuVQgZQEqS4ds3DGEbISYuCw0DQHGqYUmkzXUNA2b0rIsJstlR2UO3B6yMRggMZbspAtDxoTDQDKZNAxjYGAAIFRZVSXjJpQNR0khWcZ8IsRkrJp8b4eZRMyw82QgfXhpL0Ock9LNIYTHjx379je/2dnZyfMZGhz81j337G9ri0ajN2/c+Pxzz/3LzTfftmnT22+9xffLsCXHwxiZMVMLwzftKHS4oKuEqmm0mZlJxCgplGOieYqePHIm7IxUJZPJ3bt2HenowJe//c1vHvqP/8AEJ06c2L1rVyAQONnbW19ff+idd6648sp/+fznjxw5QrqjR1poCJCKlWpTZjSrWnGmrJGQjNKBxgAq4grJaNbqvQRutXDRIr/fv2P7dnzZcfjwWwcP4tHeuWMHAOC8886rmTv36JEje/fsWdHc/MjDDzctXSqTQTjddF0nq1qFMGql+EsGSZezfG8HSw+5Y1BmtH0+34euuuqPTz75pyefvObaawnZ9lde2fbCC8tXrKiZOzeRSHzrO99xHAcB8M1vf3vOnDmM42QEMh/5OHgOKkoha/d2YIpYzY0w+fTGjYfeeednjz66e/fukeHhcCRy19e/3vb660XFxV++4w4IYVdn51e+/GW8DTVN86aNGz9+3XVAmW14jJCYR/I6CvVVI7Nzb2dgYCASidTU1IyPj4+Pj1uWhV9pQqKAS/jHYrHf/OpXzz37bCwWQwhpun7hhRd+cdOmitmzcQeWbT/60586tv2FTZvw4kbIh/FWjMnPz8/LywsGg5Zl2baNNzxdnZ0rW1t9Ph9jQUYdRl8Fku4xO/d2eFPSwytsQiCRSJzs7bUsa05lZUFBAbFLb2/vL3/xi67OThxbL73ssrXr1tFqqDNDYWFhbm5uIBCwbRtbk5jS7/cDF/7hBkmXM+92+P9CUoWx1JHBNM2FixbxNDk5OQ0NDQ0NDfiytKzMDWc3cZC0Eio1VSQpazAboFbSTXOeT2lp6cevu87r9W59+um2tjaSdhjD8ayYtMtDVlTmQWMyunBZoAahuPRI8nD06NF7v//9nhMn+KqBgYF7v//9gwcOYCb/93e/u/9HP1q6dOlLL77IO8U0pFI0pKvcW4MgJ70ScFsaSAUIuoqhB0D8UjJDQ2OikciL27Z1dHTwlF2dnS/87W/Dw8NYyvrzzvvmv/3bs88809DQANO9khcDupjdMjmZS5SeMIU0jGpZu7dDAA8Rb0G6vGjxYo/Hs3PHjsuvuIIZqp07dkAI6+rrIYRjY2Ob/vVfg+PjhmEYhoG4wK8QxqXkjMl4Muj63o4mc1ceqa4CkmMeYZOcnJzLr7zy9ddee+7ZZ2nOO3fseOnFF5ctX15TUzMyMnLjJz+58dOffu6ZZ2774hffaGsD8mxAM88ICn3pLtTq8wRZ2+0wXFD6ph5xfnrLZz/b/s47D/77v+/etau5pUXTtLcOHty7Z09hYeGX7rgDQtjf319UXPzFTZt+8uMf33vffRcsWUJPPZR+NMn0m0HUGdrtAEptoRww025nMmKK/IIeZBqfm5t7/49+9Iuf/Wzbtm37X38dAQAhbG5p2XTbbZWVlQghgNBAf/8D990XDod/8L3vffwTn/jIP/0TLQ+gVnxT0tmluXkyfrLTAgjuYzBEULJEp5H9/f2xWKy6uprZ7TBsiQQ0JhKJnOjutmy7uqqquKQEpHw5HA6T0yAAQFVlZcXs2UIhGePicnFxsd/vDwaD/BLd5/PRRmHkEVYJ/Yn2IZjNezvKWkVayM3NvWDJEiITIcjLy2tubmY0oZsLtaJB7X28BflLNw0JZN7tMBFKTCaZ18Je+bnDYJi2Mq3oVjLBFMsMYRlINGWSj5CJq7QzjTitXp3wZhKmDoYAcdtenvl7mXb4OMp3zMQRNyGfT2VSj5b0y8xr5tJN1paJ50ZHIYEiVgLycRwmnPO9TuYpYVSSaCKUDGM6Dh/+78ceg8ojd8dxTnR3M0hN11e2tn7+1ls9Ho+irSyi8ZIwju+GCaGkh1b63g6PEcoKJMYlyEAggI/OIpFITk4ORkYikd/91389+vOfFxYW8s2JfMFgcHlTE89865YtPq/3c7feyusz1YURI7DCrLxqTDnL93YIkNq/Pv88vnjl5ZfHg0GMbD906PLLLy8sLOR5vrht29tvvbVj+/ZwOIwZYQqQ/rd3zx5eH8W8EUJGpfhLBkmXZ2q3g9lallVcXIxn8apVqw53dLS2tgIAEomEPydH2PW37rnHMAzDNH/929/m5+cDCBEABfn5psdTW1tr2/bBAwcQAIlEQiBD+sNvUlFnOu3wq1+hlHytYk2n63o8FsPl0dHR/Px8uiHdF+l92fLlyUQCAeD3+QhNXX3952+9te/MmZLS0k1f+AKz9kLpsT4j8GTCrEJryhAIrZGWdhjT8CsMfmky2bdk9ZOXn//X558vKCjo6ur61A038ExA+mAc6ei49LLLPnbttcUlJcFgEEKIAHhj//6v3HFHMBDwpm7LCKWa6gTndVdbTWYfXNZ4FN1MMc78ooSvtW171qxZS5ctKy4u/vCHP3z82DG1nhDCr9911/ZXXvmHSy/t7+ubQAJQX19/191333vffTfceCPfNQlYLkGYTt1gZEgMM3VvZ4LecYaHh0tKShzHSSSTwWBQxp/A177ylfPOP//Gm24qnDUrmUxizmfPnsWPGvzjhg18k6nGPtJcKMBUkaQ8U/d2ZHiehoH/9aUvFRYWfu873wmMjWEiBKFt26FQKBQK3ffDH2JkRj1JFy77PXdQHbJBLlbyMk2EFa5qogmE7e3tZ86cSSYSPr9/aephFZoGpkele3/wg3nz5jU2NZVXVIRCIVw1MjKy/ZVXAAAf/ed/phftMP0Eftpph1aHL6dpKkeyn01msiFKX/3SgywUiHYQCKFlWStWrLh4zRqeP2NEwuHHDz+8bdu2vz7//MDAAH4xBAFwXn39zZ/9LADAcZynt2yhZygvWEaTKSxILmXuJWwCyG5H1gGQTBCaQNaE1Pb29paljh3nzpuHb+crYPNXv7po8eJbbrmloKBg4rVbAI4cOfJ/7rqLrCXpwQCiYcag8D6GRqim0NkZSvoym/d2GDUw3rasRDKJ/5hUy/MEANx4001Dg4Nbt27FL//g6rr6+vsfeOBTN9ygaRqgmNDzUWg1ISj0pXmq1ecJsrnbwXzxM2yE7fza2pKSkkQ8PnfePE3TxsfHc3NzFfon4nHTNHNycyceLoQQAKBpWigcblq6tGL27Afuv5+oSgpT0iJbKjMw+XEcJDk0o6sUZAToYfT5fKFQ6PixYz6/f+/evR//xCeOHzs2b/58mpKZbk888UR5efmFF13k9Xoty8LYw+3tX9+8WSgYmLppFMLzzOnLDGmHRFlaILVlAWdfgJBwt4O9dN369Qghn8/X29PDi8K02nznnV1dXdXV1RCAvLy8L2za9JOHHwYAQNwF+U/ZZRpexiRrwB0n0oOkMD0dtbN8bwelp1THcQzTxMSzCgsDgcDY2Bh+0V3Y9oH773/0kUdw8/9+7LHHHn/8i5s2PfmHPwwNDU0MFfWfnk+0VARkktPyKwzqsiEBNu3wYZXoCdLjLh+nmZ4QQrquj46O2raNENq/f//CRYsaGxvJS5AMz8HBwZ8/+ujPf/nLxsbGhx95pKOj489bt3q93ovXrBGcs6V7xDTmOKMmo44MSZuFIZjZezsIoZaVK5/6058AQvXnnQcAcByHnH5DCLc89dScOXM6OzuXLFniOI5l2xc0NPzk0UdLS0u9Xm9vby+EsKi4mJnDwh2B+yX6zKYd0gcvDb8HIGYiVbLdTjKZ7OnuXrV6NUDIdpzenh7btgtnzSJkp06e7O3pSSQSo6OjGz/zmcKCgmeeeebhhx766ubNCICWlhaE0MneXnWmQNQ+wo3ObnQUEsjWmBiZ9t4OiT58uGFWxW6AUCYSCbK6DgQCNs7LAAAAmltaXt29+4orr3zqj3/Udf2H99//0IMPBgKBb9x99zXXXHPJJZcMDQ29uns3s21mLKKI9W4kVKgmCx1CGWb83k4kGg2HQjhuGoZRXlGRpEzZumrV2bNnY7HYytbWO7/2tXvvu2/nq68ebm+vrKqqqalxHOfrmzdHo1HhET3vPucyc3mzymaCbORm9t6OYRh+v394eHhkZOT48eN5eXnBQIB4KLbv/7777js3b77t9ts9Hs9VGzb84fHH8/Lza2pqAADhcHj7yy8Lco5kiUZEomVQmC+jUlNKO1k7ZOMdB0Koadqll13W3NIyNDjo9XrLKyri8XhxcTFN84EPfvD666//9A03XP/JT977wx8ODgy8tm/f5ISAEEEI0v+Q8uhMYTuGMruQhXs7EyMsGnDbtvft3ZtMJsvKyj542WUAgGXLl5O2pK/Pfu5z82trv3z77dU1NWvXrcNncbQ8PH+hgQA3RYSWdZlVaE0ZAqE10n7mjemeH2dyybaSnERpmmaYJoSQ/F5jb28vAMDr9YbDYbqjf7j88hdeeunmW24JjI0dPXKErsKeSAPmIJy8QkVkwOvOI3kmjH0mlVV3r4g1QjmYWk3TVq9evW79er/fv3vXrmg0Gg6FwqFQwwUXbHvhheHhYbqJx+P54KWXfu3OO6//1KfSWImix5q1a3mxM9qOECsMpMCou8jSvR1JW9L9qtWr8XOO5eXlPr/f6/XefMstd3zpS/hEUgaO48ydN2+SDwAAAF3XV7a2fnrjRpByfGGnQJ52mAjDt5oSkpSzcMiG5RWuV2gwDEPX9eqaGny5uK7uG/fcw1OSYMQgmQku7MvlSJ+7ykLIwr0dckFXyeK3IF8pHYdhos6HLsGNOpDb2PB5iUGK34TnjUhXCV1DKJmwwHRH4/nmNJJMWCGfafuasF/h5JA1AWS3o6BWyKcWnW+uNp9La2ZEZvBQ0fDIdBHyFPaLQZB2FDNuemmHEYhXOBIO809jKeaUkLOrmZ4pewAuKdHldyPtMOWMywi6/Mcnnjhy5MhXN2/2pL6fBdJdT+0XIN213Qz2TKUd4Wqe6RhlvLeTybUVSq675BKPx6OnvjpNmvMWJF9Xc8lZAKIdkYRw6mmHRFlFXGP6EBBQpwl8cuA50MiDBw7ohsHjaWLywT8ayacvBgQmk5zL8boTFdykBEDSjmzqKaRkuMhqZcmHLiSTSZkAdKZ20x2YSjSnTSa7dNOQQJbe23GH5KfJ6dOnKyoqRkdH+SaEWMGQkMlUkAEf35lLBVKWl96993ZoPBmeOXPm6LpOfjuYiEX7o0uewI1LntsKVA1Zem+HewMf0/Nj++K2bYl4HEK4Zu3awlmz4rHYq7t3L66rA+kWpHunm/Oy8XiM1DQNb/BJ7Mb/8XvlGXUUEvBphxYsa+/t0FiGG536L/nAB7q7urw+X35+PoSwq7u7vLw8GAjwhuO5CRVmanl1FPIzMYQ3oiwsEkp6+LPwTTYFMK4KIbRt+4033nj9tddw1h4eGlq3fj3+AWYmyQizDY1UJ6upAsPTpeJ0OQu/t8ODjKdt26/t2zdnzpyy8nJcZdv2ju3bT508uWr1apR+TC2TCnBDTnuTzAMUudFNVdbSjhoUIYxhrut6S0vLrl27Fi1ahKs+eOmlIyMjF118MfOjGcCFc9HTUx1SQfrknaG0k/ZxHJAepJklt7BM0o66CQbbtnfu3Hns6NH2Q4cIzalTp17atg0/DEOriuRAm4zvLqPOQoZqrRkCYb9Ze28HUpRMtCaXuq5fvGbNiuZm+iOdx48dq6ysZIIgSl8nMIZgZOCRQvPxSF53XjVZLgJc0Myw2+F7Yihlogs7SyaTv/31rwGEGzZsKCsrAwDE4/GL16yJRaM8W6E8amn5yU5X8UMrYyvDyJAYMjzJxv+XTTeQHq2E9IZh3HjTTXNrak50d2PM6Ojo0NDQqVOnUPq0jcVivb29J0+elM07wE1ngUgU8JRCpaaKpMtZeKQAgImb1MKBpcGyrMd//3uP11teUYEJTNMsKS5esHAh+XFw/L+/r2/u3LmBQCBALTnpHgmGKdNIRhKZVNmC7N3bkbgADYZhLGlsrK+vj0SjCCEI4djY2J5XXzUM42PXXuvz+cggG4YRi8V8Pt/Zs2fJu86MYIqueWsyXplRHUY16OYBapAemNC5vbcjlIwwjMViiUTi0KFDZ86cmTdvHgBg4cKF48FgX18f/dAlQshBCN/s9Xo8ULLogVx2QqKVaUYQGkjmXsImgKQdWQdqgdxUMZr7/f4LL7ooFoutWr2aVA0PDzu27dg2mePqvoRlWlohE8ilHQwKu9M+JNOavszmvZ2MrCzLeujBB/1+/8Vr1jS3tEw4oON0nzgBlCsB2bwD6b4vNBYvCcNBdsk3UTREWd/t0H6BuIWUaZq33X77i9u2rWhuxlUHDxyAEJ5//vk0hzSJOQfnmQsnAQNqQ2cFZuTejnBgMfj9/o9cfXX7oUP4i1eNTU0D/f2RSIR8BVzmm7zrKbx4emmH4TDltEOirDCiCy0rJIDyQy1C6TjOn7duhRCSr81qmvb2228jhGoXLDBTr6XIIr2MM5BEALWtGSdlODAJU8GHFLL/ezt4kUXTkP+2bQfGxizLIlZ7+623IIT4S5+yjtQK8MIIk48wyALR7lasnbwhgSy8twOA9A0Jhtjj8Wy8+eaWlSuXLluGyZavWGEYBv3xb8UEp2M/818oFZCHGl5Nnr8QyUhCl7Nwbwch9uFwJBpbhFAikXju2Wcdxzl16tTcuXMxQWBsLCc3F1DehBCaDBepiebGARG38uXDQkZ1pg0z8k02mWeZprnhQx868Oab82trCSsz/XtgwrZCiyi6nmra4XUUEsjWmIhOO/T851ewdAfCIRVajk/6iUTi8d//PmlZo6OjlZWVmKyurs52HJLBFWmHCMDrgxSrCxcGktHIwiKhRNQ2LAvv7UxciqqYtGOaZuGsWYZhFBUVEeLD7e29vb11dXXqz9XxbGXCTG/+MrFCNiSKvrJ3b0e+PacnQk5OTk5OTmBsDH80BwBw8dq1c3t7iR0VaUemIT/r1RPcjVJMFZ98AGefbO52SEaTeY1hGJdfcUUoFMrNzSU/U7J3z57+vr6ly5Zha7qZ4HTXwqWPorkb4unBxI95MeYAlF2AfEzohnxbtidNc2y758QJ/FEx0mRla+uChQvxShNk8kq6X0ZOulM3aScjH159YV+kSvAL6HS8cBuVlPsBQmbZdldXF0Lo2NGjBNnd1YU/3gJTIOPDlMl/PszJhBHyFEZ2ISumR5CelsUfNRWGA4U0IH0BwFsEX/p8vtLS0qe3bFnZ2oqRoVAoHA53dXW5+XVkxoJAaQWxnKkyr9e5JCsMBjNtAWdH4exmKBnuzESj+1u6bFkymSwvL8fNd+3cefr06cbGRpc/yebGCijTN4eECjK1fDSTIUl5Bh8pEEI4HM7JzbVtG6edf9ywwbKs/fv30781rR4hNxFA0Twj5bRB8Hs7uEIWcfkqwgt/YchxHD4wY8pYLPbnrVtjsdgLf/sbSq1bTdO88MILsVfyrejmjCTCLMGrICvzxAq2QoPQDBG+40hQ/ExhqqQhSSQ0T+bxeIqKik6dPDkntdXp7u4GAPT09KBMyxqZAEzakSUuhWrk0uW6SsgHkifZhJlakU8z8uXTDoTQcZxwKBQMBGpqajDBkY4OAEDn8eP4My8ZJylIN5kwubkBRdJnrOxmYDCc070dISV5RhR32dPTM9Dfn0gk1q1f7zhOKByuqakZHBjAW52mpqan/vSnygYCX4EAAAw4SURBVMpKj8cjm9p0X1ByoMAIw99xE0rOKKUwhYKe/M/OIwUM0FW7du4cHhrCAdQ0zfr6+lgsVjhrFiaoqq6+5mMf83q9+OF+xTwgVXSB7iujSLx42YXJb2fw4+B+TOgmTDi/7vrrlzQ1lZWXAwBisdjAwMDFa9a0vf46pnlt376tTz/d09Mj+010BXOhADy9sIphyCNpCwgTDs9QfG+HH0k1YK7Y9ZiGhmG8tm8fvinm8XiqqqrefOONdevW4X5bV61CCB14802yPGKkpIVByg9/0jJD7nAMujvpIZS8IkJ6Whh2jyGTksghI1BYffHixZFIBE+8WCwWDAbb29sxq9dfe+2Zv/zl5MmTbn6AGqbnCpk8Qgn5WMmEAlImRuTHQyYSBreHbDKBaK48Hvf0xv79ZWVlaOVKB6HDhw9XV1efd/75mKZ11SrHceKx2DR2OzJViZnUs0qhuExTYYgj5Rn5OA6jSWVVFUYZhnH11VefPnVq7549hObpp5766SOPJBIJtf6THXHexEoiOZqRNckWCD6OQxcU/wETgCRe2X7oUFV1dd+ZMwgh27bb2trWrF1LvBIA0LR06fza2mkshoSSQEkwZTwIirxboSODJA1pZNpTjXxu4oEfeQDSvk7LND99+nRxUZFhGBBCy7IGBwfnzpu3v60Nj2Q4HD58+HB/fz/2Si0FSBkTiSPwVbS9ZD/3rsDQzWUEdNc0UlO3VHQmU4Dpr6SkpK2tzev14tFbsGDByy+9dPjwYUzW09NTVFTkz8khR78Ze6QtyBiUH2aZW8jio6J3hUgYxGlHtnRSO6xQ1hXNzc0tLdhnfT7fqtWr/X5/PB7HBA0NDU9v2dJ+6NCaNWvoV+tlDJnJKFTJvUVkavKXbtJO9j6OAyE5EyIK4/L2V14Jh8Mf/shHIIQej6ejo8O27SWpHxxsbW1NJpM4AhCekCRiPN9F0Y2Z8nSZcVhG83NXWQhpD1DTZbpjPiQDZgDli6SdO3YMDw/n5ubitLNv716EUGlZGRnhOZWVCxYsoM8rhSBb9DDLQCDyWUVDWkeaRph2GD50jwCbEkhGWBZ0FRLwxOvWr//722/j/K7r+gVLlvT19ZWVluLatra2gf5+hFBzSwtIH07Z/GWQjD5EqkgkEo/H1cPDpHveanzqYzyJXELZA9TMgkMtDaMY0wRCODQ0FI1Gly5dCgDweDxPP/XUmrVr161fDwBYnfrAGHuWI0qAQmGwPzJ2P3b0aHd399nR0abGxgULF5KkR7OSKTIloFtpxLoYgDLWIgqYVjTQ+O7ubtMwyNsPOTk5zS0ts2fPJhy2btmyY/t22dk7zZMRQEbpOE5bWxv+8tvWP//5+PHjsqApU4qpUiDpctZ++ICoAdLHau7cuchxsOy2ZT3zl7+MjY0lk8m6+npMWVxSEgqFQPpUhZLJJcSg9J2i4zjRWCwcDo+MjCSTSbxaID9nDKbrgBkhC/d2aA15r4mEwwcPHhwZHgYAGKa54aqrPB6PYUw+q6RpGn4ZPM3X5PmXNgf+r1Gg67rP57vyyitHRkai0Whzc3NtbS1jPt6neMl5m2TU2lAEEbpMhyp+VIUzDZMdO3asuLi4p6enddUqCKHP56uYPbt2wQJy43tsbAz/DjZZpSuAdE2/9IzjLJ3E582b99GPfhT/CGdBQYFFfaiZV41cKpK1DBCTdjJmapdAXkSm2w4NDubl58+fPx8rHw6FAEJHOjoWLVqECRqbmmLRKPMYGzGNBiFZVxI8S5MyKy5ACE3TrKqqCgaDBEPSrtBqtPlkYSQjMvv3dgA1yACADVddNTQ0hL/ZAgDIzcsrKy+vmD2bEL/R1jYwMLBo8WL6tyXIEh2kLMVMCJgODCbjsx78nBWqIwt3QmSWHingpKHZ4o+AQggty/r+d787e/bsla2tVVVVuPbCiy7q7OzE65XJtsRf0u950V5JTEy7Hm1K7IO8gjObdsg1E4ZppGJMCD1/+av//M+x0dG/v/02AMA0zbu/8Y28/PxTp07hLNFx+PCuXbv6+vq0dMBLa4QQ1DRInRgRS2kUnhR0Xccfstd1Hb8qwAccWnImvTBaqBMOb7TJb7LxZj6X0ZvMAPPnDw4O7tu7F0Jo2/Z3v/3tUChUW1uLjWJZFoTQY5q0HWkf1Cjb0fYiBRpoc9NfkHApKn0pHAOePi0zK4aOYITTlg7b3V1duXl5AIBQKIRXcJDK+H6/37IsfGABU4seekt3dmSkuKSEDnAnurv9OTn9/f15eXl1dXVk5mKe/Izmg2YgEIjFYoZhaJrmOA520s7OzosuuogsxRg1hZryaYpviMtZurdDOQLj5h6PB0JI3nmidSZQWlYG07MKiZXkG4G0pUAqgBIMk2ds204mk8QKQudSKC7TVJGmspd2Ugo4jkM0Rwg5jkOcFIhWhanvG0wmaHqoSRzknZFmQqd47PKxWIyOlQycu8pCyMK9HYSQY08A4/MgdXOcscIEIAAZJDEoUZjzTY36IChpCCEke1bbtqPRqG3beGqTsXQcBzkOSA9NjOIyHaFo9c4gs3BvByHk9fnw75UwA44vycRnawHQISR/GoQQpGjSYx+9t2H0x/+JHS3LGh8fj8Vi5MYOHmzMjYmMCvdUpx0eD7NybwdCWFpSMjY6yowYKWPX4KWB6XeONQg1bfIDqhMicbGSFhhQj3M6joOfV4jFYrqu4xhN1zqOoxsGWWbxGiHRIlQNNH127u14fT5N1xFCpmniiEniFJ1A8Qpm0jQTQXLCRvgYQyY05oZnK+YJqAiD8wx+stDn8xEvxmEH0/f39dWkfitArSZ/+e6lHQihP6WAx+OJxWLYcCgF2JrYCvQ6EKRPWAAFJyPMwR02nEMBrsWeiIcKm9iyLMuycApyHGcsEFi2fDlQTrVzgSzc28GXCxctOnDgQEFBga7rXq8X/2Itmap4rhmGgd0EzzJN00xNx1aYCIu6DrV0MVIeTcxK+7thGF6v1zRN/nDIsizcF7Zjd1dXU1PT5Cbqf+a9HWLZhvPP//s77xQWFpqm6fV6HcehV5qAnOsZBrm/iABEANh26hDMmnjK0rZtHOws204kEmRVhDeF9G6HGXXs/tgZ4/E4Lp88ebKquhq/lsHbi9ZXmNxl+gIqvMIs3tsBAHi83uXLl7e3t4dDIa/P5/F48A8MEVfCTwPjiDax0YYAAKBrOq0PhBDfw4AQGrqO3+IjcZbxBVK2LCuRSCSTSfwDfdgfQ6HQ8NDQksbG8vJyxtEUirgHQdoRhl4+1sqiMuGraVpjY+PQ0NDg4GA4FMKGwyFM13WSx3HcxDdsyayE1CEuOZXQDQM3JzSAOnTA9sIWtCiwbXt4eBgCUDF7Nn4pSJj9FUoJM8yMpx3GmgCA8vLy8vLy8fHxvjNnkolEPB7Pzc3Fj0ibpkkCFn5OiExY2pTYHBBCK5mMRqNESBwxSB7DYRSXk8mkbduRSCQWixUWFFxwwQX4VWlh7JoJSNvtMPlHGHH5LCREQgjz8vLwp6WDwWB/fz8AIBqJJAzD5/NN/Bw9hPh70Db16Stsymgshu9ix2KxsbEx0jsJFzilgNRudWxszOfz5ebmzp8/v4x6XoHPn8IykJ+NyRQE1MBMpB1ZBwx3OuLywVg9XAUFBQUFBZjh2NjY2ZERAEA8Hk/E4/gWq+M4iUSCfv46HApFIhHHcSLRKDltwusBTdNs2x4bHTVM0+fzeb1ev9/f0NBAP3/N50bmUjipFclaBmzayZippw28gxcVFdGfKBgdHQ0EArZta+T4AyGEH3pBKD8/P5lIAIQMXZ+Y/pqmQViQn19fX49tJxOeSc1CAtocwggIuJFQIZkt3fswbZiCM78PasjCvR3ZmklIqaAXXspWHrJWil5cUtIYoagMT1LOwr0d91FVmLVojCKhKbIBv9xRRElh4OMvmRwto6e5Ze29HTe1wrypBnrBkVE2hQyM/GgG3tt5P+1kDd5PO1kDwQ8f0AXF/2mkHQYpZMLjGQmZtjxSBkIV1PoqWvHILNzbmfZiXsiE5k9vJRVtGbyClUsmIFPaEcqT5fd23IPLwZhSL+pdDQ0y5FSVej/tzAi8n3ayBlm7t8PT0JTCQ62MPEH6TOSRwrZqmYXHP3wcIBjFcREjT5a+yeYOFLlCQaMWg2k7jdgqTFNCYfg0SA/q/wMVWYIUr49S/AAAAABJRU5ErkJggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566911, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW090 Z-Stick Gen5 US", "NodeBasicString": "Static Controller", "NodeBasic": 2, "NodeGenericString": "Static Controller", "NodeGeneric": 2, "NodeSpecificString": "Static PC Controller", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0101", "NodeProductID": "0x005a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 31, 32, 33, 36, 37, 39 ]} +OpenZWave/1/node/1/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/32/value/17301521/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 1, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 17301521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/22799473140563988/,{ "Label": "LED indicator configuration", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 1, "Genre": "Config", "Help": "Enable/Disable LED indicator when plugged in", "ValueIDKey": 22799473140563988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/61924494903345172/,{ "Label": "Configuration of the RF power level", "Value": { "List": [ { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" } ], "Selected": "10" }, "Units": "", "Min": 1, "Max": 10, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 220, "Node": 1, "Genre": "Config", "Help": "1~10, other= ignore. A total of 10 levels, level 1 as the weak output power, and so on, 10 for most output power level", "ValueIDKey": 61924494903345172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/68116944390979604/,{ "Label": "Security network enabled", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 242, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68116944390979604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/68398419367690259/,{ "Label": "Security network key", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 243, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68398419367690259, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/70931694158086164/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 1, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694158086164, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/71776119088218131/,{ "Label": "Reset default configuration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 1, "Genre": "Config", "Help": "Reset to the default configuration", "ValueIDKey": 71776119088218131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/31227923/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 1, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 31227923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/281475007938579/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 1, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475007938579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/562949984649235/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 1, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562949984649235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/844424961359895/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 1, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844424961359895, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/1125899938070551/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 1, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125899938070551, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "Smart Plug is a Z-Wave Switch plugin module specifically used to enable Z-Wave command and control (on/off) of any plug-in tool. It can report wattage consumption or kWh energy usage.Smart Plug is also a security Z-Wave device and supports the Over The Air (OTA) feature for the product’s firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1789/HKZW-SO01_manual.pdf", "ProductPageURL": "", "InclusionHelp": "Set the Z-Wave network main controller into learning mode. Short press the Z-button, the LED will keep turning on or off, which indicates the inclusion is successful.", "ExclusionHelp": "Set the Z-Wave network main controller into remove mode. Triple click the Z-button, the LED will blink slowly, which indicates the inclusion is successful.", "ResetHelp": "Press and hold the Z-button for more than 20 seconds, the LED will blink faster and faster when the button is pressed. If holding time more than 20seconds, the LED indicator will be on for 3 seconds then it blinking slowly, which indicates the reseting is successful. Use this procedure only in the event that the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAJwAAADICAIAAACSxR/7AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nNV9WY8c17HmyaysvXc2ySavJEoUpbEWyL42BAMDPxjzNhj4yU8DzO+6mJ/gR/+EWe4dDGyPLEsCLAsQTbJJmmSL7K7u2iuzch6CHR0ZW56qbmow8dDIOnlOnFi/iMzOykrKsgwhwN8kScqyTJIkGARnrTnIhE62mLAJkrMjieSgLpQcYhY6I5G7+HZzZAADShtSwzIZgua1ZLlcqieYTIwpY6cOWspYhkCVmGK+BS22LMJ8cpwXM99RylGhdn7QHCm54SDOee1URnSBXEynUXfKU+xA8nHWSuZysnp2vWlMZUcAP4YcaaU1gmFkRzZfU6DMB5wL51eFoCPsrBxnqkohFADRZHAUZspbAc42kuFPZZYLVUMxqSwgDQI81DiQlsG9ZKapKocQMqmVFMXLdBdmKVsnp6VwUh4LrCzQtoQJRobJjaTAavCp8sgolwGBgaIqEkR80GMrD+EgZVzUPGMTVOMGLSqpV6wwZCPMiDK8ZORZ2U8lcU45qe+gHA6ykFW9ZcUWU81SSk0SxrliGQtVVPPV1hhnXAUoOsjqGVvLtLL0lKkWs9BKJifdpWySVUyuS26OSVVRpSQ8BmMMoY77UOn4qXbHWstaAKsulJ6Ta1ERR4XaWhBpxlqBfVIXZhJYZNmw4o6OMDiifKyqIAdZnqk6sGkOcjAJLQUdRdhauFKAkeVyuVgslstlkiRpmiZJslwui6Ioy7Ldbqdpulgsms1mlmUqzqlSqWoGrbhaSr2ez3zANlYN4eCbSrXcnLM/MkExyvO8KIrFYrFYLPI8n0wmRVE0m82NjY1ms4kyZ1mWpilGAA2FRqORZdmzZ89ms9nNmzf7/f6PqRGvqUFkg5U6VlVja729RXG1UEFWGlnyVeiz0iKcpx2VP8/zPM8pw+VyWZZlmqZpmgYtmaRGKFuWZa1W6/nz5ycnJ2maXr9+fWdnB3KaaqHKaRV+mXJqgtXYXepg8XLEkqd8VJAKW9PoWeo5ekATCEeoR0tCwfVW0OJDHYGDbre7WCyGw+F8Pj87OyuKYmtr6/r1661WSzUItRUzAtOLbq0sV3WQrGvdwGZa8rHgCJr/2LGV9Kpj6I7oPOawyDhWyYEf6aROp1MUxWg0gk0Xi8V4PB6Px71e78aNGw4mWzaxPjLLe/CrcrHIn6aihDqNHsvMCyFAMxKqzkOorJXzDREDUhhpt9t5ng+Hw+ScyrIsimI6nQ4Gg3a7vbu7u7e312g0kI+VMBap8y/u/aqFs1YTK41qecrUkR5F2Wiq/ciei7eyxCdw6tnZGUyAggqniqKYzWaj0WixWOzs7Ny4cQNasKD18zGDFUkcG1lZxZKd6Q8H8/l8Pp/n55QkSavVArmh74DwLAX5u/siWYo4BNchi8ViPp8vzml5Tnmez+fz5XK5u7t7584ddQvL3OHcqcPhkOqVnF8Cwe55no/H49PT062trb29ve3t7fUUofsmcKVFTwSSguo4DhZFkec5HUFZ4dRyuWy1Wo1Gg0IlmxyJ2+sRpDu4bTabTSaT6XQKFyrL5bLRaECEQQJlWZZlWbPZbDQa7BJzOp02Go2dnZ0g8kPCFY602+2iKE5PTyViwY7QVIOEk8lkOBymabq/v7+/v49wLRtGtaYGeUdJNbF6DGZaLBbMcFaeMSSwijzzhO9vXAjeAjDAA4gnzLlGowFOarfb7XYbggyvUlTBqN2TJIEqvlwuNzc3qduCSE06iE49OztzihSGFKgzn8+Hw+F4PN7b27t9+zb0yapNqPXYvpVbz05q4imIKb+2ST4qKzmHrYVTi8ViMplMJpPZbAauKsW1I7gNCZIMWdG+gRa2pEr0IpLSdDqFTEWnMqWkWWWm+q0A7g7RM5/PJ5NJkiRvv/02+hXNonqXns2Clj10hJ2dTCaA2GzcklUVIpyXE8gqzC0sbIDqUHoBElutVrfb3dzchPs46JVQzZJAkI05LGjpGE9qgNK/9CMzoJOpuLAoinDecHQ6HajHp6en165dU93BDmjCZOyzj3vggEQUFaon0384HGKS0QIczhMoTdNWqwWeQ59hFqJsSLgjcxs9deXE/OGnnTqhdgkQPl2Upmmz2SzLMs9zWt0tohN4psoZKFNyXl3UDVhpSZIkz/P79++XZdlut+HGaZZl0DRRDGSeo1lI3WYJ9qMRlUEtT2oqh2h3sml5noP68I8BOs2yP45nsluR7kFUUeWTGsLgcDjc3Nzs9Xo00FRsZLkut47xpaWtfyqEsMzz0cnJ2csf8rOz5WSymIzPhsNP/+N/anW7qpqsH5SD6ta1rmX2D+dZC7ejGSvpLAryGTWolbIxNpXxm2VZ+5wc0dnWMsb9iiA5x5yaDYf/+7/+y+L7B63ng97LRX/UaE6TycnpdH52tjw+6c7/3b//VbPTuQwwSLtHgjbbFHKJ1RdmMTqSQKOEa+jiWiMyBaTEuAdVj0YWDqroynaPFEYCj0rL+Xz8xf9pPXyajfIwCYvQmoZytJHnm63Z1kHjxl7SbMbEkGzd5QizZyQaU5Sykk0mA3zM5Gb0QCKhSk73G4wAZMuZDhieUowQ4TnfH2VZpt1u+ze/ycqyv7OztXetu73T3ths9Xppq9nIsrSRNUgZC1XLoniOHRhE17ozMp1UUwQBb5VqR4WWSaOGDCOmsFp+QtXNMqSoXxlzJo8qgLSLVKfd7f6H//xfgognpw3xFXcoEZ2BhWrqjqz/ogcqsCVJov/vlxqacffVc9yARCcjScVYJDEhpcx0kJnPiUW2i+XR+ErEBAhVA8aU1aTandSijuSpX5LLTAp1KF9Lakw4rKR6dJWc79ceyna9tFNTxyELcv3lDC2ktDEGTFWERGuyKJM5sRL5GrKsYiElbWqluCQK5mtEpF+/2Uwpm4MBkkq3fVHdIedU7nbKdoBtUJsulhByPtXfr9nS2eu5Zw10oQKoFmQ8VVxZiawAAhfSEAFSsyKjQ4gYUlankvsiyskUl+SO6ngpOh1UMj5Za6X11zoBp46vV6GsCsLAnH6UkZSyxWpuWc6uFVEWAHUORXsnDiTPmL4jXuA1mMRAV7wMqu5sAsOnhBCuqjy2ilHPaiczfa1MPpxKohFHvetgUS1ysIyPEcMih4kaYVQ2Cja1G7GQ9bFBOg4HL24TOvpYu1r7WZPZ3g7JCko/UmfLFJeSwzH8NyLLKl/0i6HIQmNpFxlS0v1+FbeAOuC/3hJx8cAq1krBjqvi4drXB89aJQAG8zw/OTmZTqf0gaO8Srdv3/7oo4/wsYeYmhKjhYy8VeHBSuhahJCJVwlb2Yw4ceHLx2ChlhiOFYLgGbCyLG/evNlut1m3VZblYrH45ptvDg8PB4MBunA+n89mM3guCZ59eeedd95+++2tra14vS6P3vHJ6ixhqSzTAEFXefLB3yayNoS1rggHg8H3338PD07CsxD49BN8p2U0Gn300Ue/+tWv5MMPs9lsY2Pj4cOHz549gwfe8LmWdrsN/wFM03QymdAHrFSrXZKo0cOlYwJJbcrUrMuceUwyv/qy7Z00tcaLonjx4sWzZ8/+8Ic/jMdjfOYPHo0AyrJsMpl8/vnn3W6XFddms7m3t/fuu+8OBoOnT5+ORiN4uhNiC77Z0mq1Pv30U0uAK/EoY7WSRxPt3gAjFnlqIGYyl2VxXVU+FqqMFavfdPL+/v7jx49v3LgxGo0APAEz8YGmsizv3bvndEmtVms0GsGX1EIIwEF9WckbpfWy08FCFsFyPj3Lvz2psqARtJJr1YBwKn+z2dzZ2RkOh9999x00O9PpFLINnkDLsuwXv/gFExL3Atrc3ByNRmmafvrppxsbG91u98GDB48fP55MJuwpQ0dy2V5Ean0ZYi2q7F6dDp9+zEojL+Ua1vvUqkrNZ2Wn5AAlsNvtdjqdTqezvb0dQhiPx0dHR+pTjNQQSZKkabq9vf3o0aPJZHLv3r333nsP0LvX6x0dHcFzl6q0fld4hcjs7CtPOZPZMRzwTKWjqq3VLWsLgJRPDibVi7yDg4MXL148f/58Y2Pjt7/9baPRePr06ffff//8+XPwitrZUW7T6fThw4ez2azZbKZp2u/3Nzc3h8PhbDZTdbEGA8Gb9fwK0sr2UyYMtYPvTikwQ2Z+Jc5sVAtWtcnKNg5aHNBNkyTp9XohhNPT0zzPd3d3y7Lc2tra3d2dTqesmoZQgSzKvN/vQ7sLz3l3u91er4eZKv1kZczaTaxjN9lt1EYM005tYPEUd2oMOkUSK8BWw6VWDriSWSzmr/Eky7rdbr/fn0wmTFQEXhrjkKD48DC2vvi0+48AsKyQRcaEnEmzsDZf4aDyMDcr0dT5azQLMS1JMKAGCmHn/Hm+RqMBHzvVJ/xkTwhlFb4tgzIkSQLfpelWn/oMawXrShRvN2caS1DZ7rJCdqF5cKN1je53VcJUA3/Qb7bA4+qtVgufNrXKDx3Ee4HgZrjMZZuuja5XS35sMciVfQ/GLny8+HaDbFtk1q+qOcNVn2AOzTD6BRj0q7qKKlaWZVEU6NHXwXteXP3dmdhXSytVWSoMdRh4SqIgbegqGMW2ofaK7JukrFY7ZxF+yx8qK/LBbyRS5oGkJhDcgYLXZ+BXbyHp4TJX7hjT6l+GIsHA6jmC6E5U8XimhipqOTKtra0aN5IoJMBXFukSuP+AM1V5wP34TXWcDH6Vb1cIwuhWf7c2xXCQrSI9i6ajjU6w0UV541moJkEi2uiVlFkJt7H4p2lKb+9hTFCv0CV0I0joTqeDEUCLNKtMUuurTdNIQuF9YFORTw5Wbj4k1dZXunMl+F3POuC5TqfTbDb7/T4MYuNDMTnYyIndL8qMfqWTY7Dn8j5O3AaYZY5DLHYZgtKRi4e5GXcV4ldy6nrABTUVvq7KLksS+8veTAtIViat+kqAyJJ/mdKjMlchND7CGP4x8V5bjfW3amqyJrmWYqJPEvuWI7Ki70aggqnlhxYhxrxWVHbAQOuqiOItKlKb0KzYS8fBAf/aBe6n+tJJFEf0lcj5NxnzNOsaQtVGNDpjiqVaYuUW65GzNVXHEdUawb803Cs3H1ATmpGU3dVGqyRoUyNbcTkHFIF3nLx69QqecJB+lajFSgw9vhKPOo0PG7FmluJq1ZnP36GvrqTbr11aagnTAl5PAq+xoJEYs3t5/nxTt9vF92+h5JjKVFMKs/QAV7GDNfS6/BJrd1VUXmNkW0SVX0mxNWpwODc9PO2AJZC+d1AtFlTg8vyqht6QkjGxagP4hlCqtlNTs9mXVr9OpZ1wUm2X4/0aYymVYHf6TxV2iSkn4zGICrefWFBKDmp2MkBmcXBVQEXbPT9cWGJQgRkI4Rz+T3LZ/QYtzGOELquv1Y1ZEqr3GdAN7NlBqQbjI32JfNTJDIGDcC0dvBKSsBmZrE6CcfhlgcnC39/SkWOlbhm3Zm0aoih7rbXPB+bTCyS6nGVtol2dWxh2VTgcX57kHJRWIkoJD3NbZdnpmyLlWMkEdCY4AP/LvWpwwEPCUJXRtcG4fmC1Bpmosl1hsjL+Dvb4LpDNwcUlDQ1V1c2WPupOFPRWIrQsNMBQHVNC1kLcC/4fDv+rOT09hZv7eONQ1uAYjS4T2Y7AtEbUcqaByCoLszbPVMcNtXEk569kAiouvDey1Wq9evVqa2sLXCITS+WQ53m3271+/frh4eF0Ou10OpCytMDLKiXLqqq79K5jMYuVBQM+SW4q3gT2hL7sC9iI/EiBOlKUWrnH4zG8snNzc/NPf/pTCAH+65KmKX39l8qhLMuzszN43Pfbb7/9+uuvl8slhMjW1tbm5qbsGyU3qhTrHOmc9ZphPwj87k/9y0QN7N2EUlBfaKsCMWkcDiodHx/P5/OdnZ2iKL766ivk0+v1NjY2bt68qa6CB1ZCCP1+v9Vq0df/LhaLJEnG4/FsNvvoo4/wSSXpM0SXGG9dVX3FGIrp/uRfdNkF/Fpo4OAbHoMcfo7Gaw4PrMDTZfv7+1BTQwjwzlSoGcvl8s6dO/KhlhAC3D+6e/cu3KkA9N7Y2Nja2up2u/CAUpqm+Fo9hsCqmlbFjUzTyM42hk9kTwfj/GsX7KPkyCSuzdGVMhXeVv/zn/98Pp8nSYKvPYfeB1C01+vJf5WDJN1u986dO++++y6LJ9ZWMPmdopOIS52wSrBGzkHLx3QtzEHUu3h88XQIPcA9qBUwNWsFjVeJzYf/jV+/fj2snughBOhyGU9fYFmlcFOKQ1eFtHTf4KKFI2Qtc+Ulzhaorlod16im4RxFr4oiAZD9dSzILMNyOt73NENY2jhL5LGMyID/JLcWMGXYTKatxLErD/ArJFVNVmh8cJYuCSuGMqsFtdcaUmD5Mcg3nlGJWSHxxWUuLM+JTVsjfdfLeEZwkQo/FhI03+AxQ2A6R2KYTNBIhGSbqnNkTxDcOMDjiyt61jioi1cqqGo1isld+KGfsizxNxLn83kIYTKZFEWuLhmPR998/fWTJ0+ePn3y9OnT4+Pj4+NX//av//PZs2chhKIo/sd//29FUXz33d++/POfVUWkvrLDRKVYwaJ8fBCmdo50PxNJ7sKSp8TfJGexpoYGU6lWjvgIoDMfP3785Zd/fvTw4YsXL77+6qsQwuHhoy+//HNZlv/r3/718eFjlcMPP/zw6tWrTqfz4MGDF8+ftVqtdrtz+5/+aTwahRCePHkym82Kojg+Ph6NR7PZtFYktXbIKqguXJWtQ1b0MKfQypjQV8Oy2JG1xNrSkp7NiZw8GAwm40l/Y+PRw4cwuNHfCGU5m82SNHnx4rnKYWtr+9bt2ycnx420URTLXq9XFMXh4eHBrVvj8ejlyx9ms9nTJ0/m83m73T46+qFWF6t8BAP3Vq0R1ObOtJVwDmW4+GGEUPWr9ErtlpK7P1kiVZIke3t789ms3W4PBicfffxJCGFzaysk4eHDB9ev33j5ww/z+azVajNW89nsdHDywQcfbmxszuezJEkmk/HGxgb8IuI///PP79y50+32bt682Wq34avHDiXiUtVpYdS+oTYdY8xbVu8QJNVGV2YdTuYXMKW4bqNn4RdmnH+VUIEmk8l8Pu/1euoNIIuOj49PT0/ffvvt4+Pjra0t+G2W0WjUaDTg93fgxpC6Iz60dlUEisMvxDUaDXz7Et002MENv596enpq8Wd9jMUE3pGgLle3Vn6XxgHemLDCVQzBVAnk4O7u7u7ubgjh2rVryGpjYwOOZXyUpF+9Wo+yXdiO1J3StTKZ6LHMP3VHlmlBM7IqLX+akM5W3SmBSJ1MBx04Sla8Zlc5rL2WiSHHVe1okxI0i2PRtRzgo7ecT/k7pQ13rLzvV+7NFLACU4pI01TahfJ3/Mqy4Ur8R4nZqDbXZbaFqk2kxdTljm9YNaRrVTOySALi3S+Vj4rCsFSNTVVb1SKOXdgEx0aXJ6mjM4eKRLsnNcTVvKRrnZmyh2JQTB3B+MOB8sMILCIkDshTajJJczBpGL0Jt71RYrVcmsVvo+gch38QLmCxLjO45mFuxitSJgso5OD/L2R1EsG2WNCyML6VYz2mKgnlSSeb1wAlIcnRT3+pXqheBMtN/SRWxasduUKSyceaBmmrSIYxE9juuFdiXLkqP8zJ2pMYUaxigB8pXKgobdX/GJ0vT7UtkjrNgi4riVlAW5taHNQdKSjiX/2rjLRGOpBCP1IR/SWqWHLQt7LEjMu4uXYv9Ae1jAQzPKsCCQtla1PH+LWAHCj8UiSR3FV2VE+af4yPFVyqQBbJaQ7/KySaAVJxVRKaEoxbaV+5+jNjhKQf+de2ndpAtWKODCICaHRHCuQU2jXi4EqIBa4lpBr9QbusdzKMDlpFWoYyy0kg/l2aICzIAjAYGSz3pn/lxkELdgZlDH/grF/X3wSptUCtOyiJZS6HmO8l9komJemS6OTK/1MtozMu1k40yqxmgS5UkdkxwRq1+c2RX94s+Vmgs7PqMZtDITAhzRcd528rUYUI1Tiik1UfJ9VOj0GKKm68VnLwTQMyQ5T42JJJQnNrPWFYZFAcxeOUelhuaVU1KyQZ5jhW8COd7cIWWpu+aXIqTqheiMtV1kdnL2Yi6ik6jTVDJf7ScSlaHjZokZpejhqyFFnbqeOXifG1KXJHKwTXQxfZZEkMoNWKnvX+9aZKwMoyK++OfJIhE0iV7w151GElT8XDO81UiXBsgiObJVUpeiKaqTie4WZhleTwk88RzlFDJnptY2VxqCU1gGp38VkFW2XaVazEsFYqybPEnwVjDpcVkeFzKfpby7V0jhykAKLCb4yJ187gyFW10xyIop1KfDWNkYQhHJ3DL2ms5k0Ci5QSx5m/HSlZUtZWIOwFLFWvhCy9/MkqrljdUy0xmJWCyYTG48oLJxPjRgZzZ6imkcVdFUsS5RnvHqcXuzxZ7llVJNWY8UycMPKxkL8+Kgjp1RJN4Vpu4J+iIeyUCqYA831MJZMKX8b3kWX78rCh1lQ8xRxBD/BY+S9NMGITy4PjM5zJtpEBIaPYQm+mj9zLsqMExrUtzsJopYWSiUoWHPpEgx4XZjLYaUiW1f6IFXxaLWTlUE/JckgFYH5VYRBjgm7ELC6XO76PIRlea8z0AwJllrayghs9xQ6Uf5JTIax8T1Zv52Skq1szz8n8lmlnxaW1xaq0Rv1eyT5ybXANpWIvPbi492uBMCNH0PhWgsWaLNhsssxdpjlyoES5xafXlZAEpPiFVN/1ZNNfrivzXZ3AbOdoQiHC50nZyqLO/jK2VJLazGai1tpuDeOuVImZQRjYsBAPRM0g0iCVABu0fLey1qlVKurSiljbNVA9ZfBK/n5Y+JgsBy+fvishsJ/NsrOhgMSUVZ77Vf2vJrQvkxWk1E9BS2iW00wwFYFV3RwwcEakRldSmH3yfb8qtPBHRGXmSeOyxLX8HROnieh+nfZHReOVSgDOoVn7JoAXmccEhAQeZl6moAWQHH6pHOox5a56S83UWpVUe9GFsuUJ1Vyk8shwlD62KmtM/PkTJK2af8HOEJSBoq4VwSnzOStjdFzN4EAyQMoXGeOUFY3EJK5HU2OWhbMMXGRutQvOxyshJn9kTluC0fHKvV8ndgKxpsOOCe3kPR2vZcU4sGgrq/8GUcNLHfcjQ916VYqJhtrstDQKJDRxvCzLi3fTqwc41QFeJgHdmFk/ZlUgqS+j0gJPiygyy8bKYeVUrCun2kxFyVXXhGrSJ+oT+mwEYXDVGhnfJsgSwPSRlFSvQdkpJoN0Z0kokFyRSeP78go9LaONkdUiyCX8kkZGzdpyq2ltCe1jI5sWk0YslmuxV82DQIJAFbs2ah1RZdWzkBZnqrVPFiP9J0yYMn7RXU8ldaPayc6ExLg6SrR7yBTQmKhychBRvnaJDURf6sLEuFcs9ypJC8nsgDyVnzCh1rmM9KEuwK0c8tfiKVUxNdgD0Uu6k1pWncxGHLCpDWI1YhxlaUix8LIKUJIklVfDqnVYWiGSfP3jmaiTcZzFexApG0geMIsEtwdkIFxrgRj7MGBQUzOSlUOVRqk8J7mTWqV91mvAtcpE3Y6ZmDlGTrZ4UitTYhXHB5U11KHEFHHSV7pASljSb5KriaWWJUe+qyVWydhZWUGpP2QtDKLTjlEB+dBKvJ4K9KMsHBapikuv0ehMVVtI/1sw+CYo0nwSHkM1YK2FahGVGcnc78CAs51fLH1SY7p06zqMV2oqlYCVVZ+XtcF6tSEyeuKDnYrECi07lpAujyVPlCFS3/iZbBca6w6HyhP6FjsaJjHTcLKcf4UZL1skekqV1lJELZmq6WsN6lO5SuO5dpa/bpT8ihXDqLYF8AVdg1imUi0YnKqCyUSk5TNokcHqgmxbpGBMQjozppTSykjH/UKbIq7K2dJqjhD/b8mPuVogZTjsBwRbEimM2rjVEgiTVC/A8C8luuTi5gNtdIMLburelrHWCAV4371jBRXqVVbUSbUzQxUe5WS1sSjF/QTLGmwLvxKpBYUudHqljGlCEUYu85sudVxl4iizWCzu379/dHQEaxuNRqfT+eyzz9RWsJacosCqI0MshyG1j/RocHOAxkqpXVNQqWpdznyEHzMWm+yAKWBVC0sNdQlVSS5J0/TGjRvffvvtF198MRqNZrPZ559//tlnn6Eyl68CTomRkyV0l6LZUTOY7ajykTvWFmYVUZJqM8vhN2jZ7Qego0xyTo70Uu40TW/duvX+++/fv39/NBotl0tfVZUWi8Xf//73s7MzlCHLsg8++AB+6M0hP0vUiPTrNM3ImBYpaK5iG0mP0FMV+A3VoGMIs0aKxHQcKjUajZs3b06n06Io2u22AwYWJUmyt7f3zTff/O1vf4MXv9+4ceO9997DX+9zpKLNhD9NYq8EYfoxUhEaBP58VkSAlNetByOp1Zm1wsWIrlKn09nd3T07O1vpFfyUsiy7ffv2ixcvBoPB0dERlSfGo0GgHIodqvEqTS+rHVuo6i6zKNGuZ6zowfkZ44vz6N6OHD6pBlLPSjGgRer1es1mc9VgQmq1Wm+99Rb8PrkDvJZqMSozaK3l73Qn1PjqNMmH/YVx/vupdE1YN0GZlPLYJ1Qmy7JOp5NllW5uJWGSJOn3+/v7+/DryXRcRq08W9sYxnhU5VkbLvE9qRxJmVYUSS7TZ5bntN5yECNNU/aDJSt5lEZGv9/Psvp7onShM37JWPe3UGFZfsS/uASn6TUVKCYogq1hbZG3iCYl/MT8GkwCQbN2u93pdOAH44IoCnJfxqcUvS4zYkyx9EVlrFTBpF6qeIF9lVFqS2OTTmBbSk0QAFZKVgnXa2cD3R1+GLs2OBxp0WFYvTBknRoZTwwgrbBQI4ZVz1L9J3lpdF9qgVTP4lqWrPEBG6p29FdZhGsBxi//c7tgMjSOVfZ8kRzmwdaaVnF1FV3Iv0tDHalijpQyvjys2uOEiDhwltO1aZo6meoLptqRfn4YBVcAAA+qSURBVJThG6ogZ621dqkt6mwV2/31c7/S+pEF1ZJG1bNWaMkkTdPLZGqMMJY8TtGS1dSJ4Ej5ZazIjxIgWX2E45SCdVm9RYIyqccxUlqTI519yVrFNI8PDtb4SMHoNPZR7hKpwkqFhqEposXrHoK6k3nUMnEthtQa0Xd2DOzHkApNMSTrkcpBTQMVdSN3rEVdVkFlQwMH5iVNojXutThGWaug5IiuEvxvdT1iJrgMHycKL8k8hqymSa3uwfoFKZYx1MG1kYgzrfahNixYGDkzfaKZehk+Dl2mE/RZqcRiiH4sSYebynxiH9E0pbgGl2KV5A7kJXPUSoJVmQCfoiguk/T+FrWDtUGMYefXLInStKbiXwV+1dzCY3UzdS2tr2qVpaLQcbj8gEHpDLbEqd9pmjYajcViAb8y3+l05JxVSYYaywc5KD+qPCOLGqOSXDfjYEb7KCkfRjoeUHQNVZezzZi5a2WiKd5qtWazWaPR6Ha78K+30r6AtkxQFMXBwcGtW7dgZLlcXsn9hzVW+bgVXx189dGAlf/SUJ+z2RQiLPuqezs5SrnRCWVZHhwcHBwcwMI8z6nEwYgkNthoNHZ2dqg8ZVlG/pr6lRfg2lDwOxWaYDgiExRPZWruMxNTB6hgS4NOisJ8zFzItqbOsJT0yxhNejwV6VGLOeXjzHcqkaWICpOhGltSJP9U5Y4S7XQkftYWBpaaOEK9WJsEsp+UeRzc0PZd7uxVS7JPCeTKLabWOmwdgVmLRPeiBxeARKcyx7DZVjaz/VhaO7pJQSmwq8ozUCmrHZNju/iMr6WrAmcrxSkx4FVhT7JNg5YNQaQX8Foul0484gi1fuI+Ts28qKIC9T0TyYFxaRf1LJVZVad2rU9OHFvbsa1lblhQhOOVH/DzVWKJFcQdBuoblnDSc9TWjAmdwHhSSZhuGB9yoWs3zlySY5xar8iNVJGkLkHT3coBJon+Ig9md5klcgM8JU0gNVFxA0NE8rRMwCLD4i+3UwVQ91KF9JlELmfjqhgSjVTzssFMrknIBQxzJ8UBZkqZx0xcxwSlaImDEbxsnEXAeq6K900k4FtrGZ/aBkI9kNWUcoORSqMUNLerVVMeUwilZEGEWilp0MjJDOQt3SQrVQu5u08Y5atCLl3OcsASCedLJvTASpjXtwlZJZM5iiutfHKAJR6y1IKhgudsNjs5ORkOh51O59q1a51OxypX1kZSTr+srhQBdKG6kVWnraqB89UEpUYu4d6vCuhqTKmGs8qqvypogeYrXJ534E+ePDk8PPzHP/7x4sWL+Xx+7dq1n/70px9//HGj0WBApIK2ytz3lpo3qxJLmPgl6ipqVWq9BL5LY1UpXOBDIk1xKbe0Jq3WbA4r6qEaoTD+6NGjp0+ffvHFF99+++3Z2dl8Pl8ul3/84x9/85vf/PrXv4bH+akYljPijWsF2Xrk+AknUMvLIhpEGjAOlTtnFGFwDUNdhtK4FlexvVmRs/JVVVXqPBgMBoPBgwcPBoPB9vb2/v7+3t5ep9M5PDz83e9+95e//IUJJg3HgsnaqxafIkmN3VXXWh61ePKfr2YtieNOhpNqh8JgkC1REUK1Mm49HA7hmeybN2/eunVrf39/Z2en3W632+1Xr179/ve/H41GUm3kTJMApzGVpU3VjzGkLmHYJpdgjZTqM9Usafk79MO58sy4NGXxrAOzqlbSo0w9ifx0eZ7nSZIsFovNzc2tra2dnZ1+v99ut1utFvxP7euvv75//76KllJ4y+J44Ncdn9RAcQK6lpUKcsz+uJ3yDn2anSy0awFE9QoTiCGJurVMKaB2u51lWb/f7/V67Xa70WjAo/dJkmRZNplMnj59SoVnIS/BmW2hakFNFmMEpjhGEtXOihJ/FzU+mLKJbJQo97Las6jl0EISB0vZKaqz3J0ZCB60b7VasBy/IYNfuXn+/DkzigQVOmIZUeaTVKTWu6wE1Ka4NUG6QHqEHvPvp0psZCtxUAaLdHBMUFNbJ0adZjJAgi6Xy1arBbkLj7xsbW1JQ1jClNXi5IdvsN3MzqrTKP9a18oIUAFPCoBn+W+9UZyxRHcmq0VUjTImsZxvsYWz4NdmswnfXgVqNBobGxt0GhUYA8WCWWnxWgdQqWrxLJ6hxUTFPwnCqb+GHTvFgOlGhUO2MX4Nwtb0mP5tNBqAujC/KIrFYmE9MsiKnBRPkpoZvvqWdithr5zMPlI8YMAGW1e+h1ubUmpqqsWfScO8q3Jw9FTFKIpiPp/DzYfZbDafzxeLxWKxoFingiqVGTVyYJN9ZH6NhESfYjA5CNcwVMc5yttZJC/6USKVI5CEVkslJpwVH2ma5nn+8uXL0Wg0nU5Ho9F8Pj84ONjd3Z1Op+PxGB89ZJtKAGe+VDWSc3Bcpqz0nwpIVqTSTVUvyF3UVAzsaxfUoCqGUDVU01NSq5czuZZtkiTz+fydd965c+dOWZbFOc3n8zzPIVPhzr5ae6SaMk3V3GL4qU6rzVcLyXyEl2IwnuxUBX5VcaWPIzMPubHQ9msY01NKTB80hNoJj9dIkoZQc0vqHqoRIKE7VD3BJjNW6ke2KeVpTZNiULxkwJPgDyPgZxWj8ax8RskhamIV2RwdLJ6NRgNxOFKGULWF5VonjXwtcL7UKN5WvgDxhRlmXrzv1+8IGF5Rlawtk3NS5fYVlqv8guQLw1DL2sgSlWnBRhgyoxhylUUxeUwnqzBAVbt4mlDysjCHDbJssLZfiRjmO2kd7FiR8lt8LOtLSFdjmi1nCR1Tp1QZmAFVGdSFaWlEgRoR6saOrJGhalFptBhWOjrA4EsrU43uRclZS8UwVbJ3V8u8BCGWvqrXU1Y2ZOZFhptqwbUzFQVQ3Wllm7qjCmJUSPlRZp6arDFrawlXOQvRBWodkQl9cfNBCs1Cw6mdlpUvk6lUHgZo8fP9JRgiVEcrYxKjkbawN1J3KxalnI6P2OSLpwlZyYlPEUsICeAxayWrmE0l6tYWHiohTQUrTVWPsk1j0k6uip/gs72AX1a38JhltxNN0ogwGf7NiTdj14Bi2ZWoOkjm0vTSW/58dtaSxEJmdUdZyCJDVgZfqPqFuqCUX2UsSUNbrnKdJM3a6XSKooAH/uCfKnD/PUkS+I43JRh3Oh0VFeOFUYOSIgrV2lprgbOzqZzp13hVBbUgJkbrmqj/JHdS3kIe1b7wRsBr167h9mi15XIJ39pfLBbwnWJwM0vu5PyFk9k5QQRI94eqEVn98zHGUU2dQ63JsoRNUxkyo1n7wi7L5RJu+Mjy72xRuU3IYEQVdKWMabfbIBO7nwdvdeh0OlZAUH2Wy2We54vFIoQAN3thBGai18HfeABbwLNLEj/XqAVsrYT3EOdOOpn1MVR3+E/ifD5vt9vUo6rkzOWV24RBOJIlfrvdHg6HkfqH81SrnYYup77HhayiMCFRTwiCPM8nkwn8Dw5+UQERPpCbi1gCKAbAWRXQWHkKWoYxXF21VwI1wZfwn0T4CYF79+7RfSWsSnku/vWmllX2EaywWCyazWaMxJEU89LWUPUuUjgPbUxN+lp1plc4f90L/J3P57PZDBkiNqBXQN+iKPr9fm3ttGqTPGbQDb6EfzRNp9PZbDaZTMbj8Xg8/uSTT+CBLLm7jDMc4TUVXchyFGl3d/fZs2enp6fdbpcWwrVftRxD8f1a0JpDCgD4fu4gLC4bGVgbzl8uqwrmIzkFRlaMw/nTGhBb0+kU/iU8HA5PTk4mk8kvf/nLDz/8kHVqqh0onocQzKsumbh05OTk5Pj4eDablWW5WCwmk8lkMpnNZs1ms9vtts+p2WzCQ7n41vRIx1w5lVrxLqsUyFvzaECE83e2s+XMo6zyJef9/8nJCVsCfSLALPXl6enpycnJ9vb23bt3P/zww1u3blnmUtW52AVEpw0qax3loOSLE7AeTCaT4XAIsg6HQ3gpUq/Xg+d14WFd6GPR8TTpZXPrq3G1JL2lhj6zjJzf7XaLohgMBuE8StS8HAwGeZ5vb2//5Cc/ef/99/v9PvIJWlJJYgJ479Bny9QGgX2EJ/yoQFRPrFtQz/AhI8h1yImyLPM8h6ud5XLZ6XQ6nU6r1YIHB7GjYe8BvkxDqyrrfGTj8iwbgRIOvpxMJtPpFCL+9PR0MBi89dZbP/vZz+7evbu1tSU7gCCuoCSGB+n+SFswvsFNHQvr2PaSM5u8XC6n0ykgFVzVgHXgF6HyPG+1WuhpbHTB/dT3K5Vkn1aKnm63e3Z2dnR0BI0P+HIwGCwWi36//8knn9y9exeeaV0VZleAX5SbrpRQbE2jvlFRWvIMItxUiVWoD9XLVnT5dDqF7Eee0FsWRQEXx4D/EA2014vxerxTgdujR4+Ojo7Al4PB4ODg4M6dO/fu3dve3o5/eKM2i/jWKnqoeWZV3KC5n00Imu/VU9aE4Hqd8WGD4fxSGK5ioeqXZVkURVmWs9lsPB7DRaG8b9VqteD9iBABVurLffM8Pzw8/Otf/zoajbrd7scff/z+++9vbm7CcrVrUc0S7wIUKcHfPLQCsNb0Uis1rdUCIH1mJaWz0BHYWch0KUlTCokO7cxkMnn58iXUePyWBzoGfrgMUx8fLh8Oh0+ePHn58uXt27fv3bu3u7sLeWkBlY+xDpipTlH+J2OVQ2vvILLQEo5yq0USlaeFzJYK8ZDlLMQtkCDvi6KYTqdnZ2eQ+jC5KIoQQrvdvn37dr/flz/sUGsl6XhHTnXwdaY66bwS1lnob+ViqPOxPKsudzioS9QscSIgso7EiOTYMwgTOftaWyfwT/Kg3U7DDeh4Wb1dZ02jTBwMZONIdDu2O0saVXLGJFSBTpVKGresEoYviiSTidmEbSqlkpKgaixb2EKqO51zIRv9VfegxQhDAyvqVZLBxZaoEyw+/kfGFi1lyekjhNwrJkWsJGZ+8oV3FJci0fGLvWgAMv/JjYPWsFmSqVajillpFA+k1hyfic8wZkQO+i4PRnA4rFbdlH707v1a3GNyV8ZsMPCAclYHg4Z1tcSixJI2kgnDw1p3yni1gtj6qEaA5Wm+lpUKHwci0dKyjupgf1VkXgbb0Kvyl5qup+9KoXNVwgNdNEoqdzyWpdv6yA7wFHYBchqSylzyDMKXPmLLVaqOlFgxslZJIwDhQqqaehCIbX1Rg/CIFAAO+Ju5mX1V0SlTZgXVLkCoAFIgYUS54VmVuYwnin6Wkyy7WICvMg/EDWyEKUI/qglDg0ZOVqWlg350KjfVVPBh6tUivnXM7OLbMZ7WWCWXSE3fBNE8tsC/drm/xUWmUlIxTU0RFRhVe7EDxhaZr2dQq31wQEwu8ZE8BgOcTekuCGA+wxiSkB5C+L9DMElS1jwyOAAAAABJRU5ErkJggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} +OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/37/,{ "Instance": 1, "CommandClassId": 37, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/,{ "Label": "Switch", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "Index": 0, "Node": 32, "Genre": "User", "Help": "Turn On/Off Device", "ValueIDKey": 541671440, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/39/value/550092820/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 550092820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/43/value/541769747/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541769747, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/43/value/281475518480403/,{ "Label": "Duration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 281475518480403, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/541884434/,{ "Label": "Electric - kWh", "Value": 0.06199999898672104, "Units": "kWh", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541884434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566931} +OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/1125900448727058/,{ "Label": "Electric - V", "Value": 123.90499877929688, "Units": "V", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1125900448727058, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566933} +OpenZWave/1/node/32/instance/1/commandclass/50/value/1407375425437714/,{ "Label": "Electric - A", "Value": 0.0, "Units": "A", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1407375425437714, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/72057594579812368/,{ "Label": "Exporting", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 72057594579812368, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/72339069564911640/,{ "Label": "Reset", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 32, "Genre": "System", "Help": "", "ValueIDKey": 72339069564911640, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/value/550993937/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 32, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 550993937, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/value/281475527704598/,{ "Label": "InstallerIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 32, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475527704598, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/value/562950504415254/,{ "Label": "UserIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 32, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950504415254, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/5629500081307668/,{ "Label": "Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 32, "Genre": "Config", "Help": "Smart Plug keep detecting the load power, once the current exceeds 16.5a for more than 5s, smart plug's relay will turn off", "ValueIDKey": 5629500081307668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/5910975058018324/,{ "Label": "Device status after power failure", "Value": { "List": [ { "Value": 0, "Label": "Memorize" }, { "Value": 1, "Label": "On" }, { "Value": 2, "Label": "Off" } ], "Selected": "Memorize" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 32, "Genre": "Config", "Help": "Define how the plug reacts after the power supply is back on. 0 - Smart Plug memorizes its state after a power failure. 1 - Smart Plug does not memorize its state after a power failure. Connected device will be on after the power supply is reconnected. 2 - Smart Plug does not memorize its state after a power failure. Connected device will be off after the power supply is reconnected.", "ValueIDKey": 5910975058018324, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/6755399988150292/,{ "Label": "Notification when load status change", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Basic" }, { "Value": 2, "Label": "Basic without Z-WAVE Command" } ], "Selected": "Basic" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 32, "Genre": "Config", "Help": "Smart Plug can send notifications to association device(Group Lifeline) when state of smart plug's load change 0 - The function is disabled 1 - Send Basic report. 2 - Send Basic report only when Load condition is not changed by Z-WAVE Command", "ValueIDKey": 6755399988150292, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/7599824918282260/,{ "Label": "Indicator Modes", "Value": { "List": [ { "Value": 0, "Label": "Enabled" }, { "Value": 1, "Label": "Disabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 27, "Node": 32, "Genre": "Config", "Help": "After smart plug being included into a Z-Wave network, the LED in the device will indicator the state of load. 0 - The LED will follow the status(on/off) of its load 1 - When the state of Switch's load changed, THe LED will follow the status(on/off) of its load, but the red LED will turn off after 5 seconds if there is no any switch action.", "ValueIDKey": 7599824918282260, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/42502722030403606/,{ "Label": "Threshold of power report", "Value": 50, "Units": "W", "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 32, "Genre": "Config", "Help": "Power threshold to be interpereted, when the change value of load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline)", "ValueIDKey": 42502722030403606, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/42784197007114257/,{ "Label": "Percentage threshold of power report", "Value": 10, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 32, "Genre": "Config", "Help": "Power percentage threshold to be interpreted, when change value of the load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline).", "ValueIDKey": 42784197007114257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48132221564616723/,{ "Label": "Power report frequency", "Value": 30, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 171, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48132221564616723, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48413696541327379/,{ "Label": "Energy report frequency", "Value": 300, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 172, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48413696541327379, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48695171518038035/,{ "Label": "Voltage report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 173, "Node": 32, "Genre": "Config", "Help": "The interval of sending voltage report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48695171518038035, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48976646494748691/,{ "Label": "Electricity report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 174, "Node": 32, "Genre": "Config", "Help": "The interval of sending electricity report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48976646494748691, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/551321619/,{ "Label": "Loaded Config Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 32, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 551321619, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/281475528032275/,{ "Label": "Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 32, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475528032275, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/562950504742931/,{ "Label": "Latest Available Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 32, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950504742931, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/844425481453591/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 32, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425481453591, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/1125900458164247/,{ "Label": "Serial Number", "Value": "0107020900000000000607034800010001000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 32, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900458164247, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/551338004/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 551338004, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/281475528048657/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 32, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475528048657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/562950504759320/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 32, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950504759320, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/844425481469969/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 32, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425481469969, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1125900458180628/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 32, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900458180628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1407375434891286/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 32, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375434891286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1688850411601944/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 32, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850411601944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1970325388312600/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 32, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325388312600, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/2251800365023252/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 32, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800365023252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/2533275341733910/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 32, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275341733910, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/value/551649303/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 32, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 551649303, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/value/281475528359959/,{ "Label": "Protocol Version", "Value": "4.24", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 32, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475528359959, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/value/562950505070615/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 32, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950505070615, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.0", "255.0" ], "TimeStamp": 1579566915} +OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "Aeotec by Aeon Labs Water Sensor 6 brings intelligence to a new level, one that is suited to both safety and convenience. It contains 4 sensing points, which would be more accurately to detect the presence and absence of water or detect whether there is water leak in some places of your home. The Water Sensor 6 has an inbuilt buzzer that can play alarm sounds to let you know when the water is detected. The Water Sensor 6 is also a security Z-Wave device that supports Over The Air (OTA) for firmware updates.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2437/Aeon Labs Water Sensor 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action Button that you can find on the product.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action Button that you can find on the product.", "ResetHelp": "Press and hold the Action Button that you can find on the product for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAGYAAADICAIAAACVqwOrAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO19S7Adx3ne9/fMOefei3sBiCRAkAIfEi0zD7lix7FTeXiRqmxSSSqLbLJIFko5SaVclUVW2aRUWXjhpJIssvEuKZcUS6Yky5Ys25JNPfgUSZCUSAsQSQAkQRAAARLAvffce885M/1n0a+/e3rmzDn3AExi/wX07dPd049//v5f/c8Maa2JiJmJCA7MT5nK8mwegGlsMs02Eub2mfTT/8IlJtYnL8H2la1uoqxt9h0DfLzQhrLDgDJ/Ovpqq0rK/y/ElwEzsRVOj7TWq+rrzwmoj3sC/+/BX6BsYbB8sT8YYQEwa82aGbnrmQEGKbAWl5kB3U/JW8zP/ESICRQGSX5GgzLYjZCTV7acbQtShVJUlLbf3syu7NnOjMlgZq5mB1xVYtaUtvMC1K2N2xCSGyV0yRYFbNbDAJgYTNQck5n90C09s8/YQYioKAaD0WA4BFQ7tiNYgMqYua6m1Wzqx/YUI3/mr4UlJdk4HZhlvbgwyVFc0hyVwY1+NAOGPP1dDBmoshytrRXlkNC4GQ3oizJmnk0nupoSiBRBFUr520IEZuawDVs7Mc3NujJDt82WDamFfPsQph9mcC16zW5kMMBaa601azCP1jfK4drcHdoDZQwGV7NJPZsSERVlURRobH5mZGfm2VRz96Yl8UyEGSDLyaOgc+Lz6cAPp7VmXeuqYub1I1uqHHRjrYfEJNZ1Vc+mBChVlGVJRM1OCWyKXerLQyqXQzx/aYE9RZ2xYV1E7Jhk81+PZQlQRWnkwMHerhcjbVB6q6jZo6tCPZsCgCpUWXZQpeslapBvz4202UTQV8KW2Qm/7MwXBSICWCnSpDTr2XQyGK2BwsylvcXMKr4STQpi1rquAKiisG3CMmIisj24EsfoSHYrV09m2f5S2E0nVAqAQMo0pdDM164KiFRhhM9sOgm3yk9LDKzmmmB1XYMIpIioSRxRyoHFRJxNdi7p0DBgUxK0EaSk6np0jTnq69BA7n4qpQjQVcVao2Gc+p8quTjJMDPrCmDLog69C7JzRiBD8nTZvuNINF7hLIiUAsDgDrubmbt4k21Ta3E7e8+SEkqLy6PUtGMwoptC5nJ3p1rNg5WBISACWOsOtMzV/uNtS3FH8eLD2lr7ci1CanccWS5GgA5autnTTAS2ar+RiLEnclVgbAxGRhOW0MNg4qBDkZg3gQyJuNTWEEtMkhze5V0qCEdUpXxVVMVmh8/fYepLoJcngwLtsBCRTKkqlK6IGyUZPU2KzAhb3MgbHue2ckNOHBIifbId+jl/3HIskowSafOAR1ggbIdad7kX2gCZqYkUSRoujBibwL79v2IR0NOZ0deTQRQb+iTz8eZIFH+fZw5OGwKxMxvtDidiyf+ttspgklQbd7vyHdkHZ/2dP9rvMynukvGCYpYD4cMxFySeC28PibwXqew2jtyVq2NlVsXvgbP+KAsEAXE3Ig5u/Q2C9blSOFU0XJxPxZSNrPQka3vRkqoJq8NZb1gIZak3iSLRSA5jSXlYFbHTtWxqtqLtnp0IZcvnKSfsA50K4boynPXpKKBsjonLOq+XwttRIp+lILfFyKlhsPIv6gyuIMZXpOCanyvV/V3vPXCWoTLhw7AdsGP/LR2S4+Qun2/m9miGUCHKKWpsO1VwiquRE4lac4cge8yeoqxb0Eo6zLH/dCtJZLBU1kVTs9P8Jna/ILtnsNeH2FoYxlrg7EyWB2Fltx2wK+ntaVrtmR69C9H552A1QLILkVUggKeTg4vn32CtTXdXLr27t71tlro/Hr/3zkWLL9bvXnhrur9nerh54/r1a1dNt7PJ9J3zb7C2mHn/3bfH27eIGaDxzvb7l96xLg6t3zn/VjWdHAZnYq3R8j0G8wEGPlCjrUtqlLj9lIg9Bqnzb5w989wz169dBTCbHDzz/Sdfe+UMMzPzT3708nNPfe9gbwzwrZs3X3ru6TfO/sRsvDM/fPaFp79v6O/i+Tdfeu6ZD65cJlA1nT731HdfPfOiEcGvv/LSsz/4bjU5AHDj2tWXn3/6/BtnD0V1kSot1U230A6Wb6qme2OGVuWgLEuLhTznFZJQqhag2cH+lcuXTj/6M6QIzFffu7R1/NiRzaMA9nZ2bn1044GHHyVFrPV7b184eerB0foGM9/88Iauq3tPniKi6eTg6uVLpx99jJSCrq9dfu/I1tGtY8eZaLx9e/vWrVOnHwLArN97++Kp0w8PhsMlcMXMs8lBXVVrRzaHo/W2ZnMcwcycoqyJmbY07sgkVpu3DIwpxq9V/wPLs7WmWUZq+lkKdWVp5mZRVldrG10oWzTAgNno4iFlkcYljj2bJbE/huDalBAArllrr42xrgDHg+ua69qtnVnXDmnErFnXhv0Tm0NK2CqdU4YWW+Kc+kVRZs+pRSr/yTZGGyVo/e6FN7/227+1/dGHBOhq9o2v/s5rr75k5vaTH736+1/57elkAqKbH17797/2r3/4zA/MGc9v/o//+t//868zazBeffHZf/dvf/XalfcB6Gr6ja9++dUXnzfy9ccvv/B7X/tyXc0A3P7o+te//MVLF946hPd4/tHvIgEGrYOENJI47u/6xpGT999fDocASBX33nfv1pFN02Dz2PH77juhCsXMg7I8eeK+I6aK+eTJE/sHM2IQ8ebm5v0n7hsMSgKI1In7Tx09dtwQ4+bW0ZMnTihVABiORidOnlzfOLL0WoLC17HehXmZ7zsxI70fwqfWKIJT/LU/8GbnwJV2ubWCWKi7wfsaK73RsbsT7iRqlwrAY+bpZKKrWTf7XzZYqqllZDyFJO6adgotM4wXyHN0YWQxyS3uD1AsGxAHi9HWI3NwZ/paEl/ty0tBcSekzR0TZ2Yr2Rgu5UZqL9q5fevlF56bTaem3Z+9+vK1q++beX30wbUfnXkBzCCuZ9Nvfv2J61evgBnMr774/AvPPm3Gu3Xjg9//ypemBwdgsNavvXLm2pXLZoBrVy6//spLJhxkNp2eeeHZ3e3bh8KWk+ZtsBiVMRnS8cTD7nTRlIiU2ASTALj6/uW3z791++ZHAFWz6Zvnzl66eN6Q2KV3Lp5/4+xkcgDGB1cuf+G3/udLzz9rbvHXv/qlJ778BeYarH/0yotf/ML/evfSOwTW9ez8m2ffvniemMH60sXzb507W1cVgNs3b166eOHKe5eWZv+O8rsunx8rK3mZo7tE8fE/MwoRAVrr8c725rHjRjne29kejdaK0YiY62q2P97bPHYMALP+4Mp79554oBgMAIxv39S13jp+Dwi6nl2/euX+B06jUADt7+wMRsNyMGSgrqrJ/t7G1lEAYN65fWvz6FFSxSKIspDwsuYmsw6LNpR5uzSHsgVACgnr6WEGkfdQk93ZTomlmMeDgg0f+kqqupwF/cGgjOtqtHEkQZn0ZKg2m7x7/T4fp5SU+GHGOzs2C97fH1dVRYCJKRqPd90ex3jnNtc2Lmx6sD/Z3zMdaV3tbt/0fR7sjc1OBFBXs4O9sR9pvLN9qCP92L+VdU8ojh9xmHPK4kJSAOfA8HkI0ekVXhCY3zz7Z9/82u98eO0qgGoy+ebvPvHKD58GAMarLz7/za89MTnYB3D18rv/6nP/4k/+6FtgDa1/49c//5/+438w2vwz3//TX/3cP7/41psGR3/w9a+++PzTYA3WL/3wmW/87leqyQTAhzeuf/PrX3nr3E8Opcq24MsTXfH5z38+dzAWrqxnM4BJFUqpZTYmqeFwVJTFJx9+pCxKVRQEPHj69JGtowwMh6P19fUHTp8motFohLr6xV/+m0ZNHZV49FOPPfrYZwCsrw2HZfmLv/y3B4OBUkqBHzz90ObWUUANy2Lz6LGTpx4A0aAoiPihRx4drh9ZboPWdQ2ty8GwKAeNhVjMzHkgBykvQ2MqknuknIRCGTG00ZwNQbI9WAkWOPl4A8mzyPdhD2Is63OkTfYYptVm7w8JL2tr1qVk5FyMcHMVbkW/VdGohdNKOOCL4Xl/hC9OlPh0QZHrwt8KBttjJ1ulwXW3LtoNc3fS/GeYJBAYxCZFM08AOG2j9fVrV7777T/c390xOs0zT3774hvnjDV06eL5H/zJt+vZFKDp5OD73/nW1cvvgpm0fu2Vl848/7QZ98a1K0/+8bf2dneMS+PZ73/3wk/PGnK78OZPn3ryOzybAdjf3f3ed/7wxgfX5gY6d6xwLir6xWTYvwwm/4+YSPy0/huLNNuAQSDa39sdb9+eTKYEcF3vbt8aj3cYgNbj7e3xzi2tNcCz2Wx3Z3dvPCaACePt2zs7O4aRHxzs7+1uTycTY0/tbN/eHW8bEt7b2d7Z3dFcA5hOJ+OdnX0vQJcAv+s7sNHHLAe0GgyLoug6CXdbJTca6XpGRen4pqawl1nXtSoKY6iz1kopc57JugZgwuRMlXsShLnWpJS1/E0smCoAEEjripQC1BLszPGy2Whjazhaa2vWWzu1yiPF3ldxdGsiACIfBrv2rMoBW9cFQEXoAKSKEpaLk1JF4FKqkAIfhQo/Cq/cEyvlkcNgp/cvq2Q4YdOBll4b04skRhzwI+PILScz7goSJfann4+Rc85xYQ0CkGGU8Ge/CbU6nW8FbopDwmIuRulzkValE355cyWUBsvAG0umhELqZHByn4NKKUutCL6rsKhX1kYQdHLIYDb5H8ISDAoUGYcHDLt30Y/kCToagVMhKLB8GAspgc6ezI1cFGWZXRPuvosFbcOmVWJ9GIYPfnH+RYfF7B1JS7wyt+ASOqFz2xvCz/CyzicBvIIq89LwbGkTmbHs6qN4LrtLKbF5o1n5vNi5K2VuDQKX3gqTT58uyXSSL+6kpL5t4gaCl7V0kqGpFcuDiKnkO1c9RyX7TA+7k0kAcJ5ZjtPYpc0uFRXejyuFue9BYsUWgtlaReR+smtwJ14nwJ6mjPUtUdQ38sdoXcgIN6dsSXmHsEd9KTOTrGiMFjGE2OWShssmwct3RmZmXRVInmHKehnnRqlR648lgQNVNv2ilA4Yk8BhQPSTwYlvs6AnY+6ofW44N9JmefOiVJOwfC1/ErYUGB6S5ZgS5mv/fafjbfK0pNGQRMOmtGhhDI1//n6vbFcSSZ9VKyx59NsklCi8xZaYiZg2YouJtGMEZi1uODX+3Rnogf/5ZrnkrrJxBmve59dCYTZOIDLs4cx4N1qkbbX4HM0ZVf6EcHngoGR3ORoXpLKGoIyPS6jRRrQEHBUiTlm4KZtqVzRyWurU6MVW0QKin8PxsggiovIamnvSRhqRzZYmS6LY5inJ25bWW+2IyBKjdAsEcl4J9BQji6EsQ0BRLTVK0jYZEQlO8+aqWOQLDPpCj8Q7ope1wcKejNYfkh/l+XPCw2Q3nMvPnYlnjauDzs6W8WREAhFh+WxTtmmzFmbX5aUC4HEl+ne+MGrcAme9zV/kwkDRnwTMoK0oazr5AIQnft1vj600RbPE+GmbYk7kQ+CYfaoO7BcQxuXQkkm6PQ8HJIZsVrF74qb1gZyWbrt19u5Urh/IMD3fgOHOcRKOJppZ1K9K9UfE/iOWagwMj5bwdEk/f4ZpCZ+6csCz/2Z5JLwZycPDRmAS4HUNN1Z7sEirLnIYaN6erOldtlwzp98m1uxALJAH0YYISbyMqI19GPD0laMgSQjpHTokSN1YRq/Im0dEeV7Wirv2XWCpxKeNWs/U2zuIqpxWweInmox5hX7/eDIu10DFYq/VY8n44XR9r1JR8qIMqVXAcW3xgLloTkD0rIn0YUat2Rob3rPYyrIXhp7a/4J6GQn+LCiG7DO75nQ30IeI5rF/yG0ldhfKCfrYFgZczCMsViyORJ7977uqyy56jukdjna+ZB5ohnsimhrlcK8q4OjsKZWXZDc1YFvbSyhtGDKrlJYOenTYhTIrX5PC5G/g9SGVbWIftHt8vJm6IaXh6NHl3whE4hJu3MEVQLhvrdCLyigokexL0KQUt2Lkag0nMxvbi9YE2xR0U0Ojfmf6h+vMNFy3McZSCbI8dHH2+Sjz0tWmCU9Hk2ooqvVtDNJipLus0E3dnmdKPLwkfjgbLOZkK8BXjy56ezL8tDLvrZEpIalFaA8g+Ne819hIBCLzPhvAqdZ5MiXE1LQ6tSyv/Tdhscgf8ytwmCiTpNxEl4UsQ/f5+CWILU1jJ2WfNfQEiv5kYWHfv4jpdC/FiOOn4CnHxDIKgZDoWhK7jVqjZggtJr5WMsGVsf5+ymkpb2lybpi3V9KdYddDQVn37KgxJ5MK9RVeRgktxV0sNp/ggGnXd0b9T9AisVG2m75ZrFEjE1oDQsTlptF2aSw3zXvDmxLEroSk6DRDWpt0ldhrosWXqOa5tP+Z1RS5ZSu5lJrlvNjesfNr3yZtL2JfYIx5Ewh9NTGjkt89u0zZEtyUm0dEsXYQLsps+uZ9sghngXkbUyak2wqdGfF0goyWP/Psv4MR2t1DIfXX+Pq2nSdZWK5RRP+Zy6UUsfftjjAyCYn4tl7ZRrTT/OuBiH6jnSiaJEzbnq06CRE8GQ3JIcZoQvhoj7/z0WnAYcCvqB0nix/9SqUfjnBy1k/a3vwS+hREPiqfO4vg8FvZuS8CavKSx8OC55hiK0SaklfQmuhggQhK8GN2V5raXRdCerIbncT8Vxb800ctXvDZ8s68jMaIy5M5efy0kJnDsVNUteP3HN2K0PdqCI2CV6HrBqzg1SL9wTNSy8lg/WHOq+E1WtMqRkSerSVK2qEhL70i6EtlMbciIAldyaXUkKzceEuQiYJjJg7vPJhrOYad7l68ujKO1gP5fVHGEdsWmpGZcSOFXXyirJknEPMpQ57QRZJAxigDUruxl/dcxXzw5ls7LL8xG/ZkpCPI8vAGAvOOjTbxCsTmgxjL+XxsI+mluSM2ZlefrY9KtAEJcSXU2phtB5XVCtEgXqNHOP3jriScH/YiDvw+XUAcpk2eslcEc2i2BFJjvUNgdz1q41+YnhJOaIDGi2VtoWde7pEedoLBdyStbndIzP6BqniYQ0CPq1PtHw0MxtB+B4wHInpzcXMunvxcPKc8nUKSj4bLhNT486d+3tRekONliZlZymco2h4OCBcLLMob6i0iABLVFg2CQCQbiq6ygbTsSxqT4YAjlxf6+gogu70SLxAzp6++mKNGR8ET7oWpicKBuI0pCSLOVJqrDAMj6/GHU1/CXCUvy0a1kO9jHkL6gVNlvY+s+VxB+oWcuWPL9oL9u1+yNmrpG0lkksRy7KTM+DuFEy0ITytlVsH/ifJvFZQuAHRH/mTYWWT0cdQWbM4gczw4lCQ0HWqDW8OWWA7rB0uRwpbzOVU2CXs5BETUkxDT4pE/nn9bFiWKM2e5UjJKRc1jyR0YOOspVu9Y0JxprCGQaMgL4j4e0gPE3CvwdtEwFqGqmr/ipfMRoSBvMpML8JFY8nXkbMzYZPJ48dwgt7JDBxq3bcwElnkgRzB5+dtqs76MpK7pt7J5IYnFvc8TpB6bqn8N9csp1Jy812w1MAfzCz/2ldt+aQ01SsOPxEsWmQ0sGmS6dkpMVCFF0OrcjV39zEdZdDU7zsFpg8gmlxc07EXb3OQbEhPBIBcaBimQcm/9F305hDvzf5V2Uxss4ch2EiCtylEectzBIhfBqI5TElwwCFoTFh2ENbtBezGgBaEL9at2MfaZfUA6xSWS1lhUwJcIsyLtUlYfDuasYWH2z3PS/Kw5zXrmlemjc3+FKm/5Nce4o7B4GEtLGvsx8lflLg0/Y0daYGQNph6ZUETKfHN0dSdNc2DZFz43IHMY3AcaYiT/K3dlcL3Y3wsOvSxEkT+IXRrZyJ/ESyHzUq1ttokulrWOjyfXer9F+9HcHRSRMrogyedjMtoflTDV8HIqOBRcRVyedhDb5PLakE+MO+RbuTzy+SUhIMAiyD9d4pGTfxtL991z+oHQAiItQZY7PT307FPv7GJRLiuSKSHY6qk0CTb8YcDGP8TQ9PFkvLLdkVJJGpdTwlIaXZAzq33aVpgFiqWkVEdWAIJ0OMGGhMgrm9RlsCa9F+1Dt5RzwGpIYd8paJ033Iq3cDsk1d4p1aJDAHe9JDVXyE6ZilISwReUL3cGUksa1+amy0iQ5Q8Pega/9IIeXS32cnV7HGK/Wsn2jeqwz/WSe1Ja5GHerug8DiSZTmuawYEwodzrliz7cyLn8MzM8TK3uBZY+KNC3kFjfro5x0sOKYTmCSCrCjetpSaVBfqi5HicAR/jcjjoqQ0v+Dh+poQA97J+w2+iE0ehsEklhiGpFa5O5uMBqUUurJKVCQWwq9tFvbJZLiPd0A0jwJMSp28dcyl5RuU02oD0rBbLoSRQxuHV2jtCZTJOPCgFgnlz7L5P7xe5TZw0gtjcXo9gcZoQC+rmZ3DujqfMQN8n5Sy4DxNy+ONr41MigS2X9XvTYV4wQBPqEn2sNnqrRkShTOij7NwhmP/6SkhKaX5FQlKCbZEri3UDcV5rz259awpnBnBDuTaRKLFmxSr4vltmO6lK5AQq60fbcRu7RseJEsJrsDWIGP00dMWj2G9Yzuhn8iFNNwWDyhXgrYtoxGlp+nnkxJ/RuFJQi7jhQi2i9unHJlxG2xBp9ABEQ0cJhSRlxZ2ABC1o8rLuSBb3uFCDt5N9DUaI7A/FYlHBUxJFPrHTTGzwHrmZBJFgpsReK4anttXhSx79SopJyKjMVrdCEvQFp5A1GzlOHxGLuZzthbYw/DCKin2Nsx9JSFP/M5wEr1haym0gZXcIB3Fm+ULBLJ6vR6eHDbdsHKhjiIjjJnZKFDUXqljEzfydynayAmhbdRJxJL9s3Qe4JR+rBJnUZF28JyjNw4lDbot8IpGhwPP6zbsPxMFzbW477nobS04COIvZmkGSK5ldJz5yIC0BDlcn2qvXLjjZg4JTig1rSc/y0tj3vVpow8kS55jRvA3LDsqFeDCEySmo8Cpq0NV8M5GXIpOjSsH14TikNwlz/HQZkC7GjmYLoyxWluwrU9KhbUuIv0tSQxIOZwYVvz4GG+BQp+UyltGXpW1cutziWkKcIxq8i/YlsPw5ZmOWLWEsS3YZh3my+Cebr/rVlTH7b4NlqazZ5zJnv16fiEyu+LSI2mnUM4m7SmaLObIPyWVbLpdWf9BZnVkn1DTIhpnLDwnZE6Ym9PJkiF4PkfZpIrz+Zl8KLT8/l+5NtBAIg6mrz8O88LmZUletbNKWdky4UXhHdiPN7ziclsvIg2WH67GKOUgXaaLTtva9ctQRxKMSEDgxGRNKGZDlPRl53bdtELG7+oiBpjbbMhr586S8L2ilNnnkQhA2TeLdUYuTFQmcindqAWlK6V2CXXl46ZZFhjeanLdX+mbjJSBF+MpYWQTNnRc8GRKFcz0ZZodEJ2di08hgDqcphLztgWUPoki2lOaRbOIx3pjTSiCN5WqxMbuozGJ6zgZty+e7jFLK0WYyOifyXloTvEqE9YYeqmxT0SepbIpqG5DSqPHqVhwZaw0f+1ROqJJUnHhTXCGCWnK37aV+72IMaxffpyWSOPGfPo6u9NZg8yWpABBcRdJmDM+iu0FTH1Qkr3otszf00MsW/9ij6FnmLQt3T/3mbBy2Xg+P8jAzig1G7kNC3n12l3fnYgZTcCeKsoaGn33/qXW4QqgLga48LjOM1RCUilxMXMf1qwM3SAdaym4lI90RlHzrV6Z21e6wKE5h0O08r35PuascLiOLXKyEIzFEBYHDV0pXCI4Nd6BlcU9Gm5RrU2elUoa0xLUx83SxabaIAxsI2LUQPm26Uqz12eWLhuRxQl5uJOewhoyqyLQEAP9REtsEURyGCyEKstNw+hztAQTUzYqlIQTFdWzMxbpsbEx5XOLK7TFK2lJMDERGTphdSwGHXNfVtZs3r+2MD7RWRXFsODh17OixjS3pBYrlcgH7KbVVgRTfGVjC9w/kdp58gRaJpmHnRRpHVGs01lvbN7938d0Xr9+4VSgolIRBoYZKDVk9uLb2Cyfv//kHHyiL0hGp3N0K0KvboXMUctI6+oqpV4J8ZrI3BrQqB2XZjV9JVN2QttFV/d1z5/7g8vvbBSZ6CuKCMFRqQDRQNCA1LAYlDY6Xo3/8qU8/dt89gOJ066wAZcw8nU64qkYbm4PhyJcnOCGtdUf8AYDJ/hjcB2VLzBFMqKazL54589x4PK73K+i1go4U5dagXCuUIlUzV7qumZlVoRT08B888ujfffghsGaWtLZClM1GG1vD0VpbLE94VEIiK/I9xCay4f/yzTxOx4gefoqCU8jqCMYD4EOqAeZaf/mVV5/dH+/ofaX0icHgvrXRkXJQEimCIqUZU13vVbP9qjqoK+bqiQtvMfOvPHQ6FimHRFe8QASEJD4xTr7D1B32Y0DoEiQ1BKlxyCiN4M4JvM+oaWDgqXM/fXZ3d7c+KIhPjEYnRqM15s9uHvuZT9x3bOMIiD482H/j1o3XP7xeqeKg1hNd13zwxTfffGjryCPHPyGsqpVhzXvosj4eABnC82Cqpntj7sXLgH6czPf+wc2P/ssrr13nSaVnnxgOTowGD5ejf/L4X/3E0ePGfaEUGbF6bbz9pXOvXRrv7Fb1lFGzemR49PN/528hvBd3RRtzMuF6NtrYHI7W25p9HN/6tcBPXbq8X7BGvV4Wx0r1AJX/7Od+4RNbx2B1FXcoSnT/5rHPffavHxuuMcDMmquz450fX7m66PT6LqITFj/65UYalVNaksszYzqZvn57p+K6JHV8UA41/8NPf2ZttN42483h2j997C/V2vklqX7y0qV8/4cAit0DWVgcZU1TqVmO+fmrt2/vERN4o6ANVTw6GD1438mkZeRfJHr8nhMPrq9rZiJoql/56CbrauH5d4K0Sdrgrhz9ZngzXd3bV8QDRWuqKAifOn4PSEkjKTjEvQKk1OPH7724N1YgZnxQ6dvj3eNbx8NnrFYHvY5+FwBq0Exkk7s2lNYdm9YAAAcbSURBVLPAbTver+qBovWCRoUCcHxtnUUbEphijzjm46M1cz6rmWuF2wdT32SZhSwFS2mn3OBSLHgbo7PEAgGlUoyiJCZgFmvUntKS21PVNYE0QwOaQU1X8OGAAN3if/KwzLd+ZT5OKVseGZ6uZGtYDqgYKFJEAK7cvs2skYkmkwvQl3ZuE1HNrJmo5uNra8yr9GTISbfBohuT/JZr/AttKJT4zUmRlgvcv7FOoAEVADTotZsf6WqWibXycRKEvYP91z+6CWDCYNBJwtbG2oLznwvzA7B6oUxqvv5VsElqal3qjkaSls5zw+ATx45vMohIs6oYHw4GT5076590lffBHgNr/Y2fnt0j1EwVE6P4G/fco4oBIQpGO0yARE/IRP60jKqNCWEYSDOFdR1CvAG70ZJtGyIaDAaPHzuuudSgikmDfu+9axcuv8cm/sZ+kgiwalx95u0Lf/zBdSKaMDEUquLvP/RAImEig2xJyFlI8f1IX/qQRRzDtCQnm7L/IM8imw3iB98YwC998lQxIw1VMZhUvb72314/96ev/3g2PXA3BwDvjXe/cubMb771Fgo10VQxoMu/XJY/d/pBucj0jHhF0LwBwfmTBHF4+RXZmJ2z8t6yhhKW+G9Dy5cvvvPEe9emqGasAa0Zkxrrk/3PHj96YmO91nx5d/fc7s5kNGDgQPNEM7MqDvg3fumzn37ggWQ44SNZRoVk5tlkUleztSObg+FaFl/MPOeEqYEVF+sfI4icU8j8yPQotLfgkAb//KMPX9rZf/Kj3RmqGlqRHhSo1tdePpjq/YlZu15bqzUONFdmw8+KX/vZT37q1ClHtBzvJl7IOdC+0FaHRf5RidYwFnGI0bQ1jZLU5+4GI4jwjz77mfpHb/7RjZ1ZoWrUREwgBWZiZq4ZFbi2xzRqWOPffObBX/nMY24eZp5yi5g5LLtFaY4hQR24tGtjnu6NAU12Y/olS5xy+6aUJSld2kKCruuXL17+3xeuXWdmxWwVrzAJzcQ1fnZY/su/8qlHTp5oUzU94pZDGTPPpvv1rF470uX86Y2yoiwHZaSbhyWl57wRYmP8NC8n98mTvYODZ99+/6krN9+Z6JkyDTWYN5gfPzL6e6fv+2unTxXtPrvD4wvAbLpfV/Vap79sEZSVZTgRsvKR4HFiJIY/zXSpa9+SD0Y4zCOdqPXOePfa7v7OtCLCPaPBia2NtbX1XOxBYzHzNk33SgFMJ/u6mkNlfW3M3Hyp7Vf8NFKoTvezX5499zRqjzp69OjRraMwGtldcLTIWfbooBfKGKyZFcLDI5EiZ05bAMhXD+fYGELq964VsQT3wgiBbdZ3zz9B7oXSc6HnSx/cPsruDqc7WLzINn7fSa3MM7zwne7wwmxyHd09bNlJMfrR6aKvFplTl+hkYXyvmBhqDPpBvIXt16QXmtTKoOe+7mOWR3fc+vpsXqYM2JdC+ZJUv5PeDbuTZXmM7rsd0Nn3VrV+uLatq/AiFBey6DZXYEgWA0zp9vKkRiKf24UfA77coOQUgLZmXR+utb00dyPFGUqyorY5LgmpFNDtaPNj2pJuOgwARKSKDjuyx2vFixIAWM+58460erSBFRahPQU/yMcBCe9Xqgst0bd+hWJpfxKRUoUx+EyMUI6LyZSSkhaICHUh1wAJ6H/VXGBm1poBUgUJlDVxEqEz6ywrytK85oO1Zh+gKk6AvFEepxy2W1rJImvapLEzbeDarJIg2YEGAB4MB9J91MRJH+cPFeVAz6ZgrZkLFXZXLOhEuQtgYf8OMpKpv1Y8s+TIxy9DLiqXX/E2ZmZd1wQ1GK13+8TLhPCaHQEYDEeTagZmripdtmJZklIo9PKw4cgg8a1fkRdWZ+TSyU7tsGAP5Jl1XYMxXF8jSr9NlUAvO9bcgenBWBGhKJVSkkG2bhXhseC4ZV5IkB2rtS6dlangPpZCMw6M3R3WWrPWdTVVg9H6xmbYAy2QBn52gK6r6eSAwFAF+Sis7om27B8bWyyMVjSf1unuWDBSCh+isCWuwrdl52QSGhXgGbTWuhyORusbPda0CMrMCLPJQV1Vlu1k3jQJSUy2wvzVGmS/5UL2lJdWFE5hvl4S2bbMGkqRdkdaLUoSg4nUaG2jHA774AuLoszNR1fTST2badZB6HVfIkb0c4Wjsqy+DElLXT2z3Z3BDSIYaLYHZqUUKSrKwWA4KoejnsiyC1gCZdFs+0qurCkQ95a55I6BjLtZEA4TZC1jMPq1//8CVvYhjj8/8Bco6wVSQen/3fL8tup2hCzU1aL93LkOu/tJw1iSkI25g3Vowtlr+6B+buO2HnoeYHf00NatXGYaxhJF9IrLEu25G60dLZPO28p7Limv0Pfrp+cSEuMcTYlJ+TjCaG3UeOapbVod+Z7kRuKRq+ZisjeyaRt1TyzBWhup+sz/ATefof5JBRJjAAAAAElFTkSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} +OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/32/value/604504081/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 36, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 604504081, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/49/value/281475585687570/,{ "Label": "Air Temperature", "Value": 17.100000381469728, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 36, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475585687570, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/49/value/72057594655293460/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 36, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594655293460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/value/618102801/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102801, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/value/281475594813462/,{ "Label": "Instance 1: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813462, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/value/562950571524118/,{ "Label": "Instance 1: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524118, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/562950567624724/,{ "Label": "Waking up for 10 minutes when re-power on", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 36, "Genre": "Config", "Help": "Enable/Disable waking up for 10 minutes when re-power on (battery mode) the Water Sensor.", "ValueIDKey": 562950567624724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/2251800427888657/,{ "Label": "Timeout of awake after the Wake Up CC is sent out", "Value": 30, "Units": "seconds", "Min": 8, "Max": 127, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 36, "Genre": "Config", "Help": "Set the timeout of awake after the Wake Up CC is sent out. Available rang is 8 to 127 seconds.", "ValueIDKey": 2251800427888657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/2533275404599316/,{ "Label": "Current power mode", "Value": { "List": [ { "Value": 0, "Label": "USB power, sleeping mode after re-power on" }, { "Value": 1, "Label": "USB power, keep awake for 10 minutes after re-power on" }, { "Value": 2, "Label": "USB power, always awake state" }, { "Value": 256, "Label": "Battery power, sleeping mode after re-power on" }, { "Value": 257, "Label": "Battery power, keep awake for 10 minutes after re-power on" }, { "Value": 258, "Label": "Battery power, always awake state" } ], "Selected": "USB power, sleeping mode after re-power on" }, "Units": "", "Min": 0, "Max": 258, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 36, "Genre": "Config", "Help": "Report the current power mode and the product state for battery power mode", "ValueIDKey": 2533275404599316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/2814750381309971/,{ "Label": "Alarm time for the Buzzer", "Value": 1968650, "Units": "", "Min": 655360, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 36, "Genre": "Config", "Help": "Set the alarm time for the Buzzer when the sensor is triggered. 1 to 255 Repeated cycle of Buzzer alarm. 256 to 65535 the time of Buzzer keeping ON state (MSB). 65536 to 2147483647 The time of Buzzer keeping OFF state.", "ValueIDKey": 2814750381309971, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/10977524705918993/,{ "Label": "Set the low battery value", "Value": 20, "Units": "%", "Min": 10, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 36, "Genre": "Config", "Help": "10% to 50%", "ValueIDKey": 10977524705918993, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/13510799496314897/,{ "Label": "Sensor report", "Value": 55, "Units": "", "Min": 0, "Max": 55, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 48, "Node": 36, "Genre": "Config", "Help": "Enable/disable the sensor report: Bit 7 - Bit 6 - Bit 5 Notification Report for Overheat alarm. Bit 4 Notification Report for Under heat alarm. Bit 3 - Bit 2 Configuration Report for Tilt sensor. Bit 1 Notification Report for Vibration event. Bit 0 Notification Report for Water Leak event. Note: if the value = 1+2+4+16+32=55, which means if any sensor will report alarm.", "ValueIDKey": 13510799496314897, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/13792274473025555/,{ "Label": "Upper limit value", "Value": 26214400, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 49, "Node": 36, "Genre": "Config", "Help": "Set the upper limit value (overheat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value. Default: 0x01900000 => 40.0C", "ValueIDKey": 13792274473025555, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/14073749449736211/,{ "Label": "Lower limit value", "Value": 0, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 50, "Node": 36, "Genre": "Config", "Help": "Set the lower limit value (under heat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value", "ValueIDKey": 14073749449736211, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/16044074286710806/,{ "Label": "Recover limit value of temperature sensor", "Value": 5120, "Units": "", "Min": 100, "Max": 4080, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 57, "Node": 36, "Genre": "Config", "Help": "Set the recover limit value of temperature sensor. Note: 1. When the current measurement less than or equal (Upper limit - Recover limit), the upper limit report is enabled and then it would send out a sensor report when the next measurement is more than the upper limit. After that the upper limit report would be disabled again until the measurement less than or equal (Upper limit - Recover limit). 2. When the current measurement greater than or equal (Lower limit + Recover limit), the lower limit report is enabled and then it would send out a sensor report when the next measurement is less than the lower limit. After that the lower limit report would be disabled again until the measurement >= (Lower limit + Recover limit). 3. High byte is the recover limit value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 4. Recover limit range: 1.0 to 25.5 C/F (0x0100 to 0xFF00 or 0x0101 to 0xFF01). E.g. The default recover limit value is 2.0 C/F (0x1400/0x1401), when the measurement is less than (Upper limit - 2), the upper limit report would be enabled one time or when the measurement is more than (Lower limit + 2), the lower limit report would be enabled one time.", "ValueIDKey": 16044074286710806, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/18014399123685396/,{ "Label": "Unit of the automatic temperature report", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 64, "Node": 36, "Genre": "Config", "Help": "Set the default unit of the automatic temperature report in parameter 101-103", "ValueIDKey": 18014399123685396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/23643898657898516/,{ "Label": "Get the state of tilt sensor", "Value": { "List": [ { "Value": 0, "Label": "Horizontal" }, { "Value": 1, "Label": "Vertical" } ], "Selected": "Horizontal" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 36, "Genre": "Config", "Help": "Get the state of tilt sensor", "ValueIDKey": 23643898657898516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/24206848611319828/,{ "Label": "Buzzer", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 86, "Node": 36, "Genre": "Config", "Help": "Enable/ disable the buzzer.", "ValueIDKey": 24206848611319828, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/24488323588030490/,{ "Label": "Sensor is triggered the buzzer will alarm", "Value": [ { "Label": "Vibration", "Help": "If the vibration is triggered, the buzzer will alarm.", "Value": 1, "Position": 1 }, { "Label": "Tilt Sensor", "Help": "If the Tilt Sensor is triggered, the buzzer will alarm.", "Value": 1, "Position": 2 }, { "Label": "UnderHeat", "Help": "If the Under Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 4 }, { "Label": "OverHeat", "Help": "If the Over Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 5 } ], "Units": "", "Min": 0, "Max": 55, "Type": "BitSet", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 87, "Node": 36, "Genre": "Config", "Help": "What Sensors Trigger the Buzzer", "ValueIDKey": 24488323588030490, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/24769798564741140/,{ "Label": "Probe 1 Basic Set on grp 3", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 88, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 3 when the Sensor probe 1 is triggered.", "ValueIDKey": 24769798564741140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/25051273541451796/,{ "Label": "Probe 2 Basic Set on grp 4", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 89, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 4 when the Sensor probe 2 is triggered.", "ValueIDKey": 25051273541451796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/26458648425005076/,{ "Label": "Battery report selection", "Value": { "List": [ { "Value": 0, "Label": "USB power level" }, { "Value": 1, "Label": "CR123A battery level" } ], "Selected": "USB power level" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 94, "Node": 36, "Genre": "Config", "Help": "To set which power source level is reported via the Battery CC.", "ValueIDKey": 26458648425005076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/28428973261979668/,{ "Label": "Unsolicited report", "Value": { "List": [ { "Value": 0, "Label": "Send Nothing" }, { "Value": 1, "Label": "Battery Report" }, { "Value": 2, "Label": "Multilevel sensor report for temperature" }, { "Value": 3, "Label": "Battery Report and Multilevel sensor report for temperature" } ], "Selected": "Battery Report and Multilevel sensor report for temperature" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 36, "Genre": "Config", "Help": "To set what unsolicited report would be sent to the Lifeline group.", "ValueIDKey": 28428973261979668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/31243723029086227/,{ "Label": "Unsolicited report interval time", "Value": 3600, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 36, "Genre": "Config", "Help": "To set the interval time of sending reports in Report group 1", "ValueIDKey": 31243723029086227, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/37999122470141972/,{ "Label": "Water leak event report selection", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Send notification report to association group 1" }, { "Value": 2, "Label": "Send configuration 0x88 report to association group 2" }, { "Value": 3, "Label": "Send notification report to association group 1 and Send configuration 0x88 report to association group 2" } ], "Selected": "Send notification report to association group 1" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 135, "Node": 36, "Genre": "Config", "Help": "To set which sensor report can be sent when the water leak event is triggered and if the receiving device is a non-multichannel device.", "ValueIDKey": 37999122470141972, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/38280597446852628/,{ "Label": "Report Type to Send", "Value": { "List": [ { "Value": 0, "Label": "Absence of water is triggered by probe 1 and 2" }, { "Value": 1, "Label": "Presence of water is triggered by probe 1" }, { "Value": 2, "Label": "Presence of water is triggered by probe 2" }, { "Value": 3, "Label": "Presence of water is triggered by probe 1 and 2" } ], "Selected": "Absence of water is triggered by probe 1 and 2" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 136, "Node": 36, "Genre": "Config", "Help": "When the parameter 0x87 is set to 2 or 3, it can get the sensor probes status through this configuration value.", "ValueIDKey": 38280597446852628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/56576470933045270/,{ "Label": "Temperature sensor calibration", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 201, "Node": 36, "Genre": "Config", "Help": "Temperature calibration (the available value range is [-128, 127] or [-12.8C, 12.7C]). Note: 1. High byte is the calibration value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 2. The calibration value (high byte) contains one decimal point. E.g. if the value is set to 20 (0x1400), the calibration value is 2.0 C (EU/AU version) or if the value is set to 20 (0x1401), the calibration value is 2.0 F(US version). 3. The calibration value (high byte) = standard value - measure value. E.g. If measure value =25.3C and the standard value = 23.2C, so the calibration value= 23.2C - 25.3C= -2.1C (0xEB). If the measure value =30.1C and the standard value = 33.2C, so the calibration value= 33.2C - 30.1C=3.1C (0x1F).", "ValueIDKey": 56576470933045270, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/70931694745288724/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 36, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694745288724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/71776119675420692/,{ "Label": "Reset To Factory Defaults", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Reset to factory default setting" }, { "Value": 1431655765, "Label": "Reset to factory default setting and removed from the z-wave network" } ], "Selected": "Reset to factory default setting" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 36, "Genre": "Config", "Help": "Reset to factory defaults", "ValueIDKey": 71776119675420692, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/,{ "Label": "Instance 1: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/113/value/72057594647953425/,{ "Label": "Instance 1: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953425, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/618430483/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 36, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 618430483, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/281475595141139/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 36, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475595141139, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/562950571851795/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 36, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950571851795, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/844425548562455/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 36, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425548562455, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/1125900525273111/,{ "Label": "Serial Number", "Value": "0d000100010108010100000004030800000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 36, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900525273111, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/618446868/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 36, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 618446868, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/281475595157521/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 36, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475595157521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/562950571868184/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 36, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950571868184, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/844425548578833/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 36, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425548578833, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1125900525289492/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 36, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900525289492, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1407375502000150/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 36, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375502000150, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1688850478710808/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 36, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850478710808, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1970325455421464/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 36, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325455421464, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/2251800432132116/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 36, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800432132116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/2533275408842774/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 36, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275408842774, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/281475595436051/,{ "Label": "Minimum Wake-up Interval", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 36, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475595436051, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/562950572146707/,{ "Label": "Maximum Wake-up Interval", "Value": 16777200, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 36, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950572146707, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/844425548857363/,{ "Label": "Default Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 36, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425548857363, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/1125900525568019/,{ "Label": "Wake-up Interval Step", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 36, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900525568019, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/618725395/,{ "Label": "Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 36, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 618725395, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/value/618758167/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 36, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 618758167, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/value/281475595468823/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 36, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475595468823, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/value/562950572179479/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 36, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950572179479, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/,{ "Instance": 2, "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/value/618102817/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102817, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/value/281475594813478/,{ "Label": "Instance 2: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813478, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/value/562950571524134/,{ "Label": "Instance 2: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524134, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/113/value/1407375493578788/,{ "Label": "Instance 2: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578788, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/113/value/72057594647953441/,{ "Label": "Instance 2: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953441, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/2/,{ "Name": "Send the configuration parameter 0x88", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/3/,{ "Name": "Send Basic Set when the Sensor probe 1 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/4/,{ "Name": "Send Basic Set when the Sensor probe 2 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0005:0002", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa005.png", "Description": "Aeotec TriSensor is a universal Z-Wave Plus compatible product, consists of temperature, lighting and motion sensors, powered by a CR123A battery. It can be included and operated in any Z-Wave network with other Z-Wave certified devices from other manufacturers and/or other applications. By the built-in motion sensor, an alam will be sent to the gateway when the motion sensor is triggered.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2919/TriSensor user manual 20180416.pdf", "ProductPageURL": "", "InclusionHelp": "Press once TriSensor’s Action Button. If it is the first installation, the yellow LED will keep solid until whole network processing is complete. If successful, the LED will flash white -> green -> white -> green, after 2 seconds finished. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once. If it is the S2 encryption network, please enter the first 5 digits of DSK.", "ExclusionHelp": "Press once TriSensor’s Action Button, the Purple LED will keep solid until whole network processing is complete. If the exclusion is successful, the LED will flash white -> green - >white -> green and then LED will pulse a blue. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once.", "ResetHelp": "1. Power up the device. 2. Press and hold the button for 15s until Red Led is blinking,then release the button. Note: Please use this procedure only when the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "Press and hold the button at least 2s until Red Led is on and then release the button,device will send wakeup notification to controller if device is in a Z-Wave network.", "ProductSupportURL": "", "Frequency": "", "Name": "TriSensor", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAMIAAADICAIAAAA1GKkAAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nJx9abPkuo3lAaXMvGvVKz+72zE9MTE9jvn/P6j7S4fDjvZbarlb3pREYj6AREIApSqPwq6nq+QCAocH4CKK/vrXvxIRAGYGIPelFLmRJ/qTTSNZ4q96ya9YX5pFK40Z3RP7p80VK9rK1S0n3scsW79aDcT0VjAr84+ndFmixtBTXVd+TWkLcYXbZF0VdRVi/0wRQ8ycUpI/bZ4ICyeZ3Lg/nb66JWhd3YpigyWXPHdNirqzJds/f0Ra+8TpV65YuMvoDOPk2anFldmVM/bqnRapulR1Vlf66xbOtmqRLKOr0trGqcldEeM21xaQhedimd26Ygf6rtW7xnA6EjFi7Qi2iS2KUI5id1mw2+VsvVH+WKnN4rp3t8k7OtwyUxQeG0iy5Y9RTVsepMuN7uriAGuld2uJatpiha3LatxpzRkyalzY1z53pe100y2IxLY4iOy0iAzdRuV0VdS9j9p25WADN1Z496fLJTcpViM63ZKp+9wVutVjrEzYuCh4gf30jjhdjVGhtuQtZHQRrHZ19LMDiG53t5V222Ur6or3g+VE1GINLDLXVvmO1Vwf0LxJE0WbqYViN926XMudxm2ySEL/bCGuwOiMrOeK3TFaywIipeT6a1dF3bbvNNNVtKMcJ0y3sbHeLY6P1UVS+G45EXCqmeSyuTzO00WasYXqT7GD7oDmRxq5czkA2dL2C9zpeVj3pajfCHT0esKWsW1pW1zikrmbCIItYuvWuF8dQgNtLveTXAkBzk5xNttWN2Jz7VRmsczrYCKi4buc1NWp5Q8Hi9ioaAxXzo+0FBvW+u4T9ODeTdO9/8Gry6k7InVr32FNKSepRiyAHFx4HWeoENGQkQO2yDMi8rsUsoXvrWZbX0DbjkAL71ZkmcDqwemXe8OOH7HTjhv6p65ob14PaCxBRrJw5fz4c/kpxeopuLCuglQsC3PXU3l3VNl9LgGN3DiXsUXdW9cO7Bw4bC3d9toSflASC7idxF287rRo58kOubqHO5j4p5hV7dgflGk6N78SpbHa1ye2nC0yx4ZdtRCdAnU9aYvMulV0+dImc9FrLMFeOyDr9njN9V05tRN2G7XT2C0duidbXWhLnu5NN40gZDVS+8F+YInRNi+iMD7ZupwZtOT9zuG4xFYae0xXki3GUl60KnPidWWzxnY9yrXXPq/GSMkJH5uwxa/da98TRbDudOytMuXP1Ugtms3S+H7p2DCqyvddOrViOEl+JIDYqkWfRKt3S+tSQiTLrc5qRdUQxGqvCxQy8UCXe3ZEjVfX0nrfnRekEJC45u90IdIBvy0OBj07PaDLdd/tH1sdy2aPNtsq57uVOpN3n7hytswcC49z3zuJrbW6tce2dDvSVvqtJ+7qwhe9oNtZwZnM3aSt/hFb5drQdXOxAUotkca2FErrMSp29bVlRXUQXbaIVTsJtcvGlN8lCVvRd/uVg69rY6yoy3bYNYGTZCsUiX/GSmMD5d+kkSwM6LrEHhu5hRtXWTf+0MbvMDOtmdaCb0fCbuBC5lory4u0/tMitf6JbZN8V35nJNvBLCt0FeIU2K2uq1XbM63kW7buQnZLDLKxEQystgq1KbuIjm3owllwoGPALRtE7bgSXC3dpqJDJyqegqNfiKQykQTLj9r22D2iqOsCr5bb0pLqcEsbMICzz38kS+zMXcp0JW8p1l7X6UdXrkOrVlCzmdH4VgeKlt4S1P30HYnNalfUo0V2tCgRdKosUAszfJlaQFfaHf06ZFiRXK6UklNgt9/qc0st6FlKf9rxRGSunZRkvNNOL2Xm0Zkkgin2Ffd8vwJnaVtdt9lbpTmq6/bLKJKrhZkLl2ulJJxDICYQSd9Yt0Ar2mpvTNBVhdXAFqlstdqW020jjBodkWz1N5femSZW6qzmJBydpiJErJljg7s6xQZo7L9daaJG9vuWy8sCDPFWjFxKLnnJOec5l8LMXJpLYwYxmEDMDCKAiRIIlChRSodhTETDOA5DEhb7QXu7ZEI2W03e6u5b6HGQ3bKIdmDX61yWSHKuKAcdhyFqmxABjFudLILALVFtNQM9HnIt6bY8/urwZCEeaZIBBi+5LMs8L8uSl1wymECFGWCUUgoX4SNGEUih2RGcKssncdmUCBJWE9E4DMOQxmEch2EYhiEN1q27tkRLoNkp7rrs5trqfjZlN0uXY7ZK9grsuYWIyJgewIj1pfi6dnFT0Jay3OXozWktqt51C4sVm6CXt2YsXC7TdJnnJc/yuHDJORf5hzMXLkWSFyAxFwarR2NmIFWKAgACCy8R0SAENQyJBDmofw7DOA7D4XA4jIeUUiIwW68B23R4PhPq7HdFVUgpRfkspnFkFn2IU74DorOXKyfylrOO/XNUceW3lJJ9LSSSylah0bl2O0SEo+t2NmNsZ9T1kvNlvkzTLKlLKTkv8zLnhbmwRiDiwlqZDDExAUgAg4okoqtTJGbOzJBYCgCYiCVCH4ZDSkQ13CeAhiEdD8fjOI7jOA7DMB6I2mCvtsCt9tQ2idkipbneaxEjniFyj9PM1rXjE13/d0QYLag3lY2sWDFnfB6JUR5KOVZcJ4ETJXYaV6Mqrqap3b0AlEt+ny7TPDMKA/M8z9OSS+bCIFaYQNwdCAClBLSNA5B7gICCmpol3K5aYRQQgxOhgAggZjCXnC8EMBMIKVFKw5CGaZ4SDURE4DQMh/Fwc3NzGMdxGCkMo4g60W60U/wT62Gy/Tdm6ZKHQ4AaTtcQtzptF+715m9/+1vMEzNHnux2hS797pfvxIqOL+qoubCJueTMl/l9npZSCsBUo2GgkMTbVyoCy+8s6AEIBAIXbu7lSh6AuDkQ1TIqt3Cp6AQxylU8ZgioUkppGEbxckJdaRzHm+PpdDyNw0i0SecwPcfpNroY5wQihUd9dtO4K2Jly1j2ur5g5ODp/KjlWP3JZYmV7binqLXoE63jr/UySilvl/d5WXJZpmmepgujgNOVflgiGwhuGhGxjVYIBGKw+jdBWGUNkihHcjUwQhISidcjAJyYhfmozR0gl1w4z4sQ3jCOg+Dq/P4uMfzxeLw73R6OhzEN0V9Eve2QutVVV6XRIXaZKVrZBjZOhq6rHR1ZdVGvVuz2ANfyyJyRySKebNsciO2TwuX1/bzkPM/z+3QuuTRaYQAoVP+iwlwHWRVSDDClJFsySCKimouJkJiKkBNA4EIVSoJLpkSVz5AYWQBHSEQSjidtM9UZhMRcmHleZrDMJZAM8grn8/lCiQ7DcHM63d3eHsZDzdmIrgsRauPlSNgRGVtPLDIcSiIRbDnHjsdwTi1W7DAbXVv0Yl3pnRw70I7liE1LKa/v53lZLtM0LzNyZiESYnCqXolAlLjIFECWkZw8LlwSEaOAIZG10BWAykzyuBAzC/KoesMqNZlJfwm/6j2bddwksTyaJy0mGRgE8JDGYRxSGlIiopQS3d3c3t3cjuMwpAEblyOJHb1tadgWZVM6hX+35I6B/va3v3Xdh8NHF5Vdibs/7ZSz4/LswyUvr+fzkvNluizLXDhLnMooLN2dJVxW+SsJMYv1FCnVJ1Fizo1+Wq2m9jq13aBWWGCEGobXSAm2UpgYSgoiGQM2lEt7ikiVKFHCOBzSkFIaiZASnY7H29Pdzek4pIGImAtRirZcWZEg3cbqPKrXebF9nDljxZJd9tHVhPWY3/qXHSShB/AtxrKo1WRdr1p/Ai85n9/f5zxP87LkhZkIAxfxSjJhI46EpESZn65eglicXaLEXOQXoYoa53BBIjBoIM6AUEblEBbPyOIIW7BdnShlsIRFRCQhFjFQHaDE4s3SuEZZ1SECKBlTnkXa8ZCGYcgln98vRHQ6HO5v74+n45jI9AFQ6LHNF3cM4RTuQBC9mLWgxjBbhrPX6AzpgOkodKtbaHZXfZQjChGdrvtpyfl8eZ+XeZ7nvEjsKmaVkT8zJSBXfQqEqv+hGhMRuDovQz1CRGJTJq4cxM0mVKN0ZqaCiiGgrvMzETHVWSPmUvdKUEEhbqBhbhBjcC1Z8At1xPVX5nle5nkhzMOYxvHAhS/TnFI6Hg53tze3p9uu9iJorEr3A5qdq+tVXDhl78fubzGOcwQTJXbVd31W7BNRJvfnvCzn6SIqXpaFGRCeB5PEMUXi6gFtjrA6lgJKDFnPr0AqwgFg5gISzCExGEljJQUBExNTkVqA1Ca4Ux3OocGCm7MDgIHoGmsTErcJApOGqlusc1GNZpDEQS/zMk/LMNI4Hg7jkZkv8+Upvd7d3Nzf3o2jHxVFi2wZqEtLXTtuFas2dQjpzBttcQM2YN6tstsJbDOifN5zg+a8vE/TnGWZQ8i/xre1u2uMXCd4xCqyhl/H8dcBF3Bd0q8hTQGImSDDNIask7QBvUonzFFQox+q7q7CqNTEfA3VuQ28mJkA5urzqiOuaTIRscROKNKQGpsDzIWQQBiH8XiSCSdKiW5PN/d3d4fxIM7BhRxbpunGD+6Jy9gFq7Og/np1app5K0xRh7UlqGuSrS9SYheF1nUunN+XacnLLPFQtSxB3U6LeQl1jb5ZiBpEEqk9a3XVz0jPBydQIYAwyNJJhScVZp1FqA0ozGJXMEG4kAEUiZyq2xKaVJi2GQh5zhJxtd/arEP7G0mGdaIJruE8z/O85HwYx9PNDSi9nt/Pl8vD3d3D3b3q1unQKdnhZss3de24FeC6Qvqb0qNA+lBfR7TJbDzURbEVzj2PbSMgl/w+vy95mZZpyYukBai9LY4aVjITadhDSTwViICEhIb7RFqXVFeIKaVEiYDEgIz7AAIRJVAiSqmVyKAEKiklaoF5C4qAVNmwTkLWIiR+kvoTkbAQy3Cv6kCnEYhlea7G3QTpRkSoazQA8zIv0+vLy/v5wlyYy8vby6+ff5vnuXY8rIzqtGpN496asveRIJQ1LC/Ei8xWwFWhDhnW3rEyV431TVaIrnB6b8camfl9nnMuy7TkZeGSASRQqoMi6ccEJKKkNhXmSBVFYIDqLjQNVuojAND1EKIhUaJRWivjK6IkkIRUVAu4Du7q4IiIkKjF9lUlVNrE51VLKQmJEcmCXeUeibbrbHqN1Vo4B4a0rs6mFzCXy/T++vYyzRMzMpfPT1/fzmcRTRWr7zI46zi4qKW2Di7T7DGjhUptoPvbFWENDwMmW5ZNhgBBl3cLZGoHZp7mqTDPc16WBSDGAEaiVFetSNrG9f6qwra2Udc1iKp9k/QWsWzb4SjuqTQlFGZGoqQRNIuEDGJKLHBF5ZnauBZOCROmas6GaHBSxmQ0U1UEVphQBQ6DJMQv1WnqZoOrtokZzCglv5/f387nvJTC5cvzt5e3N1WdXtEi7sb9G92Ci22o52GuMHJV2hS27sgoKndEyZb3tQJFmeQ6z9PCZZnnZZkkFRGIZBZP4hwZBAmG0NwB2qo+FJQMmU0mjXoVwC2gGio3kKykUvWXNXqXYgbQgOplKkaVnWqsBkFkc58tiqtNI50EIap0SdQWjokGYn07Z0DtIU3/1HiqgpO5UCm8zPP5/JZzJkrPL89v57Prug5P0RZbiHEmdsCwT2wt17d9u3gi41+toF1a0mQKJt7YM6mFuGIv88TAsuR5npnbPBBYiYC5OTGSzVysnNOCEgINDAJKopQSgzlB9+Jx9SrEKRGImahuS6ockqu/SokErlxDeotRMMmWW6RKP6Rq0eUKqipN4sIqb5U6rcWpwUQaqUOHei9hewNbjQiZmZEZhZlyLm9vb8sy05A+f/s6z7OzNDUHp8Z2+t9iJndFbKh91XzXtywi3DS/Y8tuZTYXmSseJuH4idsQ/rLMBcjLMs9TkecsEU4Cc1LHJ0onUErc3Je4uCQrqGBQW0OQfkKpWVoj2xrokkTBbW4arfsDhZFBpW5fKyi50UKTvEbBlBtHou5iq3oQKILr/xtHEYQ+qXpzpkaIAKc0kDr46vJgw7s69QWW3cDv7+d5mVNKX79923IFrq86K7hcjix4HRtFo9euwut4uWtsLd3CMKaxf8qxMrZwu6Mtctucc+GylHmap1IKmAHd2QOSwKg6HQg0JGBS30NEpc3IND4ACEWBWq1dGAAl2ZIt4RSjbisBExcBcLVenexOIBrKFYPQyW4SnEs5LJgUlLSoTWaOKuFRc6PirQUa6nbRnC9f6+FanQwsuM6K1c5Zcrlc3kspz+eXXHKEi7WOswgCMiTB1oZxd28JrA74IyzU0vahO/LC+ThsgFp/shXblJk5l5yZlyXnnCUgIiEXDWPkvY06hoH9l64cU10BcWI1USWIJGFrojQQJYAge60hw60k/jGheiuWKUeQjBFJvCTVFQyAq28iLmgeCo02WvjTBgV1KkJCrjo/kUiC+ToXQC1oQ/s1XUtCArE+AOoijVzLnKd5Jkqfv3xm5upNeywge+r2DeRAZv90EFwBA2tIWkBUvmp1OyqKUnYh4uqzAum/c14YnJd5nsTBU42aZTRGLbwlGeXXEVAdiCuUwUmgV7EgdCEuo8nPdbzDsrO27sBmZjBSoz6AiOp7RQNBB1ZAXSUBg5FKm1uiK8fwFScyxEMqADMnwtAUBaqhUmpwkT133EBSA/0KTDTyYxC3GVGCccK8LDPALy8voomodhucOPtaC1okRRfpbG1/vZ6LDRNQq4ElkZ2z0spsQY60HM7sPl/XsMJ8WWYA8zxP0yw8BJQaFNQwoC2TNcUWFpwUkr1ERNe9rUTkO40YQzYbyTo/CnMicC2BWvRKzIWqyyD1QPJQ0MglN7GKAoYBcKpzThKvy3+YWkiEurWXsAY/tDvU1FWauq5LNFSve8VTYg3MmIlSQeZS5nkp80K9SDde0f9EO7onXVRdIxYYfOjTSGXdWqMv60ZLVmjn9eY8I9GSl3lamsFLo7UW0RK3aEhUabzdlahqAhCLJRt5IFGdNpLSuRSSNQeiulxKQA3FmVLzc5QSpYYh4ZtUZxCJGBkVeLIB/Brn4Cp3Ip3PlsRok6QNTpqlKhmogwQZrlVN4Dp/RCyrN9WrgWUxjhnTZdoKXNzlXEd0bc5kLkG3zOtoXIJii0dHPPaJvbHgc1OoUXr767wsDMo5z/Ocy8I1zYAWBbRheJKZ4+rhUPu6PEgSGtXhm0BGYpprZcxMoIEoIRElhixQtBwCvUTCGG1HEnRyGWiTQ5ABoOhtAGQRrY3skVBnjq6bKiXsZ0hJ3HBTB2kVQCBq0w8ly0MCcxoS6hIcqjjKz0lHbQwUUHl9exmGoVnwaqxuRKEWd/Rjg40t9HTZbnXwVgyc7b3KZOuzWaIDjpDSX3MpmTMzL/M8LzPae0BVcXWFpLINJYltm3Fb6bJI0ILwFg1pvYQW2aL9WdEn6wxUh3+c6oZ/JFmMAwgFkEmqRCRhFFMd+MnUUKFELEwkcVSCpGRqiyi1HlToEyp8tWGo3FkllUi9tkUMLKMLbqM5AHURkWTiuwBIl/f59fWc6obJpsgedNyf9ia6ox0ecu5odOksICwbxSJg8K4pHRBtyXbthiWsJpqWeZ7n2iWRVoXX/8sySF2EAHTZndoWeyLiulmsvggiUc91l3WttA3rqD0tBEIpMuZqFgYYSIVBbb1dQt+6C4VKqq4woUB2lYj3q9E16WJI3enBanIxfxq4tI3aiaiwTFlWgrrOEwgnaTxUNAxPCUiJCzO4cCmZn56e8rLIVqSoefvQ4cba2uZSs1KIlvpeKDKHc7GWXbRKB1tmdu7Mioj1B9oAzMuChFzKMs3cpol1QbtGCTIEklBWImi+xtl1zkfYpu26J0kOgDhRXbeg9lOqAyLU8RRbXVGbf2LxMOIYy/VlbJlKTNCeriur7bUTiawZlFJqE9B1pqjBvxRViZAX12hc/CczErUXcblFTwBzoTb3LTMIbWWEufDL68v5fGbWbQWdSNSZ1WLFPVHbOfw5yzp4+VDGeS7nsyLL7Qsd0QZgzgsTl5Lnacq5vo1aVxba2KdVUP9XQ9dEMqnDsrJKdSIGbUd0JYk6DdMITRyEyN/WTrh6vBbEo0hMIwN9MBMzCEObRmYS9yOBNgjgSq5J5s+5Dv0p6WprqhEP2osARESMJGt9dYBYp72VfAuXgrahV8I1gKkNBaj2xsKZmUvh17fXl5cXZmbwMAzWwNrbd8KayBFb9rVPrE3lpnMqtK3b0pqjuC6wdqSUayl5ybkASy7LskiMidTMSSZigVhPQg6JQOusM7XIk+tifmvFlZ/a7y0ikjK4xrfVrLI4p8SmfR+oe5dYvVmdsoKwZN1cy8yCrnrKhLh1oUMN7nX6PdW8sjhybQvJK9syoKgolzY2liMWyNZCREBmfj+fX55fSy4AuFQYqbGtdbYiE+tYusOjyD3x1OXKRrYszckmiHZO1BJVV8oojaaf5plSyrnILBG3SZu6yoQ2vqoj+jqV0hwDSELelCilFtHUgLUarA24a2Ii2ZLd+qOELJzQ9vDrWkqtk9tQqr02gDaXzQywLO0mDA0rYnpuSx4s294qKGUzCDPkfW9GXQORaXMU9a0yq1bX4KqaUwvU2siCZd9S1fHlcnl6fl6WRXWbzDtuajK7KhVNZv+0XsihLdrR5e3sD4841YddVoz3FnzUej8Bc840pFzKPM9lyVDbEmR6pmVpQaYYJhHqmxd1RanyS2OJip/Wl7iAUI9PY5YAmlCko5c6+EFJdbQscT1VOVo0JogqVEBIGJizDNipBXCJRpa3/4nreyM1uL5OS7Zd/4S6aEIoBC6gVHdLlrrMRzK93iamZFedbv2vAZZMLiADuEzv356e5mmuMRQjpSQDfgcj69Gc93AEYTNGW3dBIlcppTPgd5m1PucXu37XElJFXpuBZdR3snJe5mVqWwTF57RNEhVVdVuFsjeo7hxq4dL1v6jbRRJRAuuO0uZfZKAtXQAMqmcUNX6iIqAbBI6luqE2NQUmQiqlyBuK9cWmusyBBpHq/aRq3XQAgEmnuGpUBwJSfcmktM2/VeABlBKj1LEdqWcXVhxAxKWAKS/55ellulxqq5rRUrpaZKfbc1h83QmeNMs+TvYWy2L+HUhG6U1pTEAuhRm5yF4i8WWaue6iFhZCDUXQDNKi50ZSNYoSKhKnUcOdto4qYbLETdW3KVlJgsTVPIkYXArxtTpZQ5T1OdSZmzYpCga1l1GQiNseE4BSav6X5DUShQIjM2TnJYFUP2ihF4HABVy4ntbVXhGvEb7styrMQGF+ealDMzWQgO44HjTkcPZyLNU19HctGynj6k81aQxoXHGWFW1BMXEI8QjAtMwMLkvJuVQAtelfM7NWRVKukc7fmiJzJ7peRRJd61KHzDC18Z1pTs1YqYpXqJS4Vd4waY8YbbtHFSG1TUgVA3L4HzGTvF0kcUw9QIJZluwSUBrs2hu3JHu9B0BgwdcpheoUC1OSoEwmHogokQQ9DOa3t7eXlxdLJ0TEgv1hiDay3GPt5W4iROwT3f5mUWiL7UxYaeYYG6kcDkCavptL6s05FypLLqVkkvmXVsD1laEkwUeln3a1tZG64xHQIb2uUlLbr9Y8mDg1olRkyYLbGVB1QqZZrsZtde1cGIQBYJAwSCYES8XWWCpWJHZG83PSH+rrSqizjoUwyKE5SEQF9cwACQTrfCcqXdWVu1QRXyejMmGQWLvkBcA0XZ6fnnLOcAEoY0hpHP0RjBYfWJOCs9qO6WEQ02Up7o7UYq02gQuSYvqOK5WfGKXo59IArpzUxmAa61Qrt2i6PVw1sm3apBptC0WwFlpFTS1ubqM26Nq5jPXqYoQQiYYqGjMxUQEopUFcVt1yzyoQiderE+WlbizSkB0ycBMcCidR41addWyM2OpH6xDif+UgQhDlnJ+enqdpij6IiE6Hw9gG/NYQmnKHL1wum3cno9EAOlMFCBG05T2tQNPsoK2VJ/t6uOTMpZ4LmyhxjXoa52uIUyszslGLetdRPECkr1dT/QONqeSc0NQiKo26xORSUQNPDXSvcRmAOumIik6NZwiyha2eP5MIdaSdrppgiKdj5uvkaJ0EMPW0UYLoh3VmCAwgDamCnhjA88vL+XyJRiWiBNzd3tmH1gF1TazJFCsxbFKKcQTB6wv6ZsiWC+sWDRP9WAzZFpqfWPFRZLhdn1BbPpJxdV3iZJVbXKSUIKYhtChIn4qN2ykeXFpciuqziBRTqDuTKiNIkIJSYBZMqmxAi9drhFx/F8EBoqG6IQaYUCrEJFq7llZboz63ToqyrMYVGdNz3X15HY4SuB6DKxQL8Nvb2+vLK7d3Ta29iOgwjHd3d2o4e+NsGqzT8XqONRyq7K96XZ2aA69DSeQ6W7STYC19TZYEFmkAEYMLM7TS6pFWupG/+MofjWQaLKRW4na8Q9vMIW6DdTCoQzD5OVV8peo/BGilDqqoDvdRZ56U22q4ZtePtakppUQD2iASsi5Sd5WUOviSzbcSgbVO2ohKm0XgUqRV9U8QMM/z05PMNF5fWb52debT6XRzc2Ota7/ApLCz+Ivo2YdURJgtf3VwrDW/xZYmkCd2c75Nj3C50Cwv2bbw6jxYuiG1La4sAQQ3DLZ0V4eTdBlHgSgjcFkdNaxpfOV1jkWI56q+6/t6JIFuqrSXgDpprbyYqm+TY2VaeFRDZq6jM4m4mYkGLgu3swVZ9lSTzCC03UnETQNVgCENlIi5MCPn8vT0fHl/9+gR6xS+u7kFcDwerSmtk5HL2cKmcbmwxpA+tzvSLDrJvmDUxZP7zpCDtkVMBBYMtljInEuW88KJChdhlDYzBJm1d86c1hvUdXRnth2ZqsUDXf1e9TnCWw0kVA3PTNA5QdRlXubqqZrEYvzMmSEzh8S6BmM/k9hcPalzU54bBiQiLnUwyAxQ4dxGag1dRDCZuWqjvL29vr68WYte6Z/55ngcx5GIDoeDBZnVv9uQuMU9zot1mcLaXdNTe2mQYin2Yay768scIdksREQ0MKMsucGTA/8AACAASURBVDCP4wDZLMNtaRzV1KIpHcXpovyKitmiSKxHdU8PqrNhZm4jdkgd+rYaF0j6urkM141IV6ypRKA6VqvjQpUnNcxLHA4ph4VeQChMRGlglgheVobFAxeqM4ttpEiJubaKUmLUocj5/f3p6TmXxTZWbhLROAzqy06nE68vtYJdsrXm6xKBfdilBnspz613iq1RbGHhnujXzWGYk9dxupOGSJYnS55nAMfTkcGllPq1XnMoBWuxyha6W1k9T/MxtYNes7TjIKi6metbSswtwGmTBSwRVQt9rw1s3kW3oNVdItX3UFv8qFIwN7kJSTcKJKg5teimYPnHjERl6lFGcc2H5Pz6/DZNU31NpsqofQn3bXSWzKSRNYfc20NgIqvpn12r2QRiLG7LvbbY65c0u9CzNKNrxbXlIc5ysqr71qYScSkFjGWac843t7eH4xFALqWUcj2wgVq/aRKpFdjqsXmRZsLrf9mQUTMkkwbhym2yb5aak5J01aGl5gbZxHB1OMXVDa6WZ9DGWtw8XJsEAoNkmArZsZKSgKaG01y3NVFKaUhciqzuvb68vr6+rkiiaQfM97e3GmUOw6D3+sEFZxQbk1jjWkdkQaZvlelzS2a8Dmz6S7NKNtZz2f0oO0QVLpsy1R3uzNM0nd/eiOh0Oo6HA1C/KUREYlpKSb2LVsymOlFokhUwAnQqq1RP1Np0dRcsYyBVrsx0owUpaLFyi3VbMMNgHlpTGhfgOu/drAAQkOpqvc4L6cKb7NQuKIImnRpFXY1FtTSYy2W6PD+/+NOkmqs/HQ4SDMk1jqO1uoLDuYjWS72lOEQvFN6a3yEaZh5tOqyRq08i+txD95NNoOXLa4icUs5ZvHVe8uvrW/1oy+kIRuZScm6alYCnjaGoBsbXoLs6KYmKC0BcmJIc1dm2YDd/CpnTKyhUkmwd1tmplrIGQmrjuouM6xRk/XxEdc+QHa2smBEagDImX2ccisbxsk2EODFn0gOTqW2sAqT5y7I8P73KhLWzJRFxzrf3j/b58XiMOIj2toa2Vo5Zuj7OIgQGqdBD+yic79/NaT2urdjWqkX1VAACyYtpA4E5AZwXfpveKGEchnEch8MhUbOFStJeB9AxgQRAAOrevzbzyzoyg25RqV2+3someklZIy2gBTb1b43UWxtaERWz18Zxi6drLro+rqOtIjxRcmn7HxM4y+vU8iKa7GWqGGNi5vP5bN3Z1WAEzvnh/t5h5Hg8OitEZ6IWiXzj7G5tZ4ERgajXqMhw9VmQOie67b9gk7nGENoIJ6eUqJSF6xiGAS6Fp7y8Xy5y0GxKaRjHQb6G115BrMSPVeFy9gy4HhWrCJZ3ZLmGrGjm1LM626seZOQEFZlcF+tqQ1pkrpMJgtok5xeLQ23pBM5U11blVV1CKSkRX+M4qq/+knwkCen6tQjM8/Ty/Krrr1bhnMvN6TQOfglWJ41sEMPM3a/jWe05M9lCrDWt6SPCVk5tK6fm6dbdvRxp1StRSkPb9EiURshKrZw6XUf7BYxS8rIsuFwqfRGIBvkM3jCMaUjDOAwpMctMAaOdUc8l1zf+S7MXUP0R1XOMYEUyEGiSJ3E7AA+g0sbxTdHpyk9CeXXjb42+Eur2kOaIwVcvWYiSBIDQ36TYMTGYMzNzzsvT8/Nlki1pzOaAKPGbt6ebqHAZ9m8ZSGlih1QcU2iCSEUxC+TTM+7nGAM5mSJRxeweQwCYD8O45KyDdjlhsWSxkpiD6kc2SotaWM5SlHNEZ9RINKVBQDUchkMa6hh/GMY6GK0hEzUiTKDEpchCLUG2vwr31JU0jZkByNJVBUCiBizU3SaAGkX2mBWuTdUzsGVsVkHW9n0LMelLd3WGnInq4d7EKOf389vrW10bXAfLKOXh/gHhYubD4eB5q0HQFhKN4iDVDY+srZ2r0T9Xkw1d7ooo2a+pK5A49pvTzbQsXBbUVVLOQEpj4cw5ywktbdcH1XMagBpptEIZhbmUuczLpE/FCR5G+QpeAkYwyz76UkqdQyZicPsgRG1M2+jT3FXDXQvrZfGv3TO3sz/ENqjoh3YtCfxlO1VqzkswKp8NqCoBc9uBKW6yMMrlcnl6+rYssxlUtDEp850Z4dsrbe80ckGqDVf033iOWbTv1kP9gKn/ntoWd7knkbG+y08SFt/e3Lydz6V+pAHtGOk0DEMhlCzvNrbVn2q8Gqug3bKitE0fLIWXXKbLhZDSQCmN4ziOh/qBYdSuU4QamJHSULikNvejK+dgRhVKFlW1VwHXPXGgdvge2hwitf0ftZake+iYkJjqp9fafCZjSAANiRiJy8JEZeHnl5dpmmox635/HIb6vaxwyVdJJZlOGlmLaEqLGEFYHNXbcMrx0BYYWD72EEFqAeSQEUFj6SemdH8eh7EcT+f3cw1ACGD5TksiYhpIIk6Z3wZIRtvcTiIXN6PH0NQFhes6ueyr5pwv83RhokQ0CKIOh2EYqH3SoZTMuq2f5D1XlgEYVX2B0nVtrjCrTWX+p/IMQc7ZL0RtUDgw18kBgZQ4Z9QtDjX2N190ywBKyd+enl6eXktej0uaO7u7vevECQCAw+FgbRfZwgFFvWT0gw49FkNdN6UpR+eDHIawAcCI0MhDsUy5v7u5KZzPlwsooZTqEtpQGtVByFtX8k5hYS7NpCTv/9S5Gmbj+KDYamEtF+Yyz/M8vb8TpTQOw+FwHIdxSEMuZSlloAT5CnY7RZvakgfXtbdWbSWJyoDVEBovtzBbmKuu/lew2RgeddO/9jrGkudvX5++fftm1wmu2ivl8e5+C0PMrPOQXdtZ5UcodL1H/GBwNL1lkCsb2Zoc1+3zU9e12eoVatQC3pzz493DkIbPX7+cbm9LKQIf+ThDSQm5cFtGlY6bkpYj4XANwGtVgM4gEhFQv+0IjWNlnM95ynmeZwISDeNhHI/HNKREqXAuhfWLkInaC18su8ukKLRJabTjZ2qDtWpxee1dytLe2pRxHZc2Ry/pJes0Td++fn56es5LsdbVm9PhqC+gcQvq7CWTRluWdj9p4bw+JDgmi/+q6WP5o+MMa/WuO7ShmXKjShbltqtgWlfO+fZ08z/+9V///o9/gNLxdJS1NlAiLkig3A4M0XiCroY0a2NX+3Hd8FraMS8iip7CJtXXHd6Zcn7Pl2kiQnV642Ech2Wep3kZx/pSrHzaM2usVvRtIAnDmYA6QYUiURKhfiepTqhfqYiG9tJrkU1Jhd9eX75+/Xp+O+ecVYcWQwTcnE5XBQa4wMw9xhgjQqdLAXp1vx5pk8XICZaNHJdarFghumi7ImYtVq3y2pdXzZCS/9e//c+nl5dffvllOAy3tzc5IzcvRToyA0D1pVNwQ0adDah1pTZpKEeUcV2u0CxtmxqIqH41XQiiMM3TNE/zO9EwDOPxeDweUkrLkglYyjKkkahS35AGOZlP4us6Q1Tk5GJhJfmnTmyjHhQAtJfzwcjIJef39+n15en5+XWaLqwodVYs5eHh0YFGL7WUm3vsxhgRMdE3YY02m9FVXTdlGN83OkZRQMif3YkHizmLHie6q1tY1E7OApgu0+3x9Jd//z9Pz0///cs/SuHb+7shDUMSJ9MkqREuEZUWbieiOruCpIeXE7X1slpJdTNEKhjVoLceli1hEDGY88LLslwIaRgOh8NhPByPB4DykoXdKBHxAHCqYVHRz6e1b1EQs+xvkYrknEYZJnDOpeTl/X16e3t5e32b5kvOVX9ElPNq0FRKfrx7sAfHoncRkR3t0+4AHr3+j4A8yxqxuhiuQBdDtjiwK4H9cydx5M+YRdJc3t9vTzf/99//Mi/L5y+ff//8eVqm0+nm/uF+oIHr9rarh+O2vCUPqPozroG2xEpoH3iEvkVZA6v1dm6JtNp7kgxmzsuyLPM70jCkw/FwGI5pHND2a2rIrN8fASBbt2V2itSNMhgopZQ8L3N5fz+f398vl/d5Xkqpkx4yHnCrFsx8OhwP4+qA1y4gUkq675HMRJF1STq7E6nF8p8gWqcMUvsChTWiC6P14bi1IutEic2IyHNU1CXVLpkRUSl8uVyY+Q8/ffrTn/5UyvLl69Nvv/32dj4PiQ6nm9vbmzpxwlx3EtUunuvArk7rJI2huHnF6z5aoqKvnHALqOrIXFgDLZ5hBudlmZdMdEmJhjQMw0iJBtkJWZfyCFTqAcXMQClMzJlzKSXnZZnnZZrn6TJN87QsuRQ5RaQwUz3vlotTr4REuiVth4cERtcAfFvh9snWucKOqNRGjiZsHKy/jggocfhwvNJtzxZFuYc7PIk2EZznRV7yf7i9/fC//z0NaZ6Xp6enz18//+PpH4nG+/u7h8f7YUjyCQYJdcGcuaQkK50MkvOsW2Amro1Q5xbM8m57FQStZ8lyRZYfobsFcpmWDL7Ix0erYySi9pq/zFeVwoULl7zMy7LkkpdFhoD1NAkpq610MOrRjeaDm0SEUh7vH/Z1rtcwDC6yLrLbvTcwciaOXkh/teMnm97VrtgYXQqbzfm7LhR2fFzEn8OWrQXtC2c6uMm55DwJFh7v7z/99NM4jpdp+vrt66+//vr69jqO4+3d7d3t/Xg4cKHCi8xY1kMVJKiVby+kegY/1x1kEg7LFLYc8SYEKVPiuaasrxo2T8nMXBiFM5jrsY+lFNR3gjKDOSPnUv0ty5lo9S/hytLItL1/rfZuU4Lg25sbd27a1sXMdouI2kv5ZotpumhQq7n4FQFwVrDKRl1BHQ06KbvsEiV2CexlC9E2tzmYte64Fi7jKYB/evzw86dPKaVpmr89ffvlt9+enp6I6OPHD3f3d4fxwKibhgky68M1ZCECIw2iDz0WRIMSeXOXmQtT4UIFJdVZ0brUzyyvBzGzHnVEaiDm6iNZ1v3qVxzADVWZM+d2AETVlbApqG0gH9Nwczw5K2z5NQCyKBv79j4EowUje7kDJCJ6LGT9vFH0aJonbkZzfvdH5NZ7jfvkYfewgViMpCxTTfzx8cPPn/6QhmGaLl++fv31t19e396J6Pb25vHx8Xg6CZ4KZwljwTIR0A4bam+7iRz1xSEmOX2BJYxBkXeLq60YNTRGfQWIwdzIjGv6mprlc39ccpF3zkCJEqf6h6zKXUmZCJCXzrCOULcVUieNJE25bu6jUspgXumPIWlkl66vQM+xxJvR/h09Ggyw3PPYQiurk2Ynl8uONdJt7bHPAZjnGfMM4NPHn/748x+HlKZ5/vLt62+//vqP//5vBj0+Pt4/3B0OB2ZwydXmJTPApYaogMROumENbeFLwnUJ5kWjmRlZHB/La0B1z4HQnlwy9V24bi8ikv+X5rOpLaRo61JZlo+Pj2l7sN29dNJIo121utK8hZc1UAxXrLEcGGwa63kohtgudQysHAydTN3G72jENju2P0qMXRQy8zLPC8DMnz7+9C8//zGldJkuv3/58tvvvz0/v6REt7d3P/300/F0LCWXhQuzvEcmx8dSXfBqJ3rIXHSNjyglFAbJ0RKFipynn0Bc5Gw/PU+QILsZkZA4MfPAKJy4FDmjjdsSTWprcGVZ5o8Pj/LlOPTC3i3F6k4jlyVSDodDp+0souUbC6DIQxaRetPZpxLbEPG0xQ3uiQNK9JgOrBEikfC691ZlRMSlTNMkf/7806d/+fmPaRzmef729es/fvnl5e0FoIeHx8fHh8FoBEQoda6SoUu+1dilGbjuBEnE9S0kAg2cmCmX0jYYtVP9mWtEr56Ti6y5yUcfMU3TIaVPHz6mtonkBy9RnfnAQ4dLFB/OZBYx19g0zNFoyWziEAepGhthl0gigFyCreyxGa4c2/jod1X6ncggUqscM+VWL5l5nhdaFmb++OHjz3/4A1Ga5unbt2+fP3/+76e/UxoeHh8fP3w4Hg+F6vv218mCFmG3DbdEROBcl9ca2JjrJqWaRWe0xSvWZd8CYlnWTSlNl2mepg+Pj6fDEbguptjady5mltG+Cz9iCGuVrLaPxxe7GUQtx+nZ3iuf9fdiW9u4GAWtcxFR23TTRzoCl7iGaWIRLkbZEX9dRLaLuB7Ot9UmMJdJw/MPH3/+w8+H42Ge5y9fvv7y269Pz8/jYXh4eLy9vT0dT5DghgEi5iIH4jLpMTSiTYATpUxt1y5YDm5ri4CiMK7HviVKpZTL+7TM5/vb2z/88U+r/o229Pe9Sz2anSKyClQrSILuS2fq6WzXVQ07Z0frvZQ2cSmlP/3o5shVMpk5JmuZ9qqwVYHzUxEfWyRkG9l11ZrYfXK09ftr0BqvBtyrlpdlWZYFwIfHx0+fPhHRsszfnp8/f/786y+/Anh4vH98+DCMiQkloy6+0fW/SY4oll1PCZCWcnV6pdQwKA1g0Pv7++UyD5Tu7+9vTzfXLZci3rr5W62wlx2LsVn0cKq2arQ67Ka0s5fa1a1g8nU8Z53+Jlq7Ios1LJjXux/aOIbQZk3Mey0Wzq6L2MKd87L+2OHAFYg1vGJR7ieYEY0D69xiqQ/3D3/4+NN4GJd5+fL09fffPr++vaaBDofT6XQaD4chDQCSbFEqiVKhkgZCKcRJ5htlKS2jYJrmaZ7yvAzDcH939/PHnxORnInuBFNbRuG3Lp006gLCdVTBh7WjJt7SJ9YsEA2BZsp+bLS/0AaAZF+p85e6gNrzcQ6LCASjf+6f2rwSozcr4UTFNrxsb7O1zPM8zzMRfbh//PThp5TSkpe3t/O356fn5+dpmuZlyaWMQ91KzuBSMjNx4ZwXibrHIR2Pp/u7uz8cfhrHxEx5WUopi3xM17To//ty+x5jaU5jETpROc4RKQNpXpnjtvrn7u7HreaxuaFGwrUgAPCgjvfWrl29dElIiRQ9DLnCHXwjdrEGn63OwU4unX04Hg7/8vMf//VP/0IELpxLzrnMiyBqkS4kXzSrr4eXsuQlLznnXDLrEuyP+6ytS7M7NopNi8rpdkL7r/NZcmkorS7PFsU64I9QdRCmFnpcX6oxARMxtzN4fWBkjeeeKM3uYXdjMteRdlc7WJNctJ/ThXu+TpCYS86Zl0V3TAtW2ttmzMxlEcwVPUsj0kBXkv+/S3YaxQ6z4yhtk2OI44gfrQ/LjWzRlKbZQwFTSn6/EdYgjeW2l/darS2upi6R4CqNa0BsofNxUWsRMQ7oO7miii1fbiXWkF2XL6yKHMmhxu+hkGDIHRtH+bcuIhqGwZG0M9yKAtbN1xeMrE93ywb2JSSLG5eR5exHZxgnkBZqG3BtDQN8dXBRHZEMupPU/gQWI7H2fEs/rkzXmWBCaVsaeqiKlHm9IStSxZRj6HCtAnmX0tqsK3OvwHXpRlRrWvTAqs+VQbNuuDS5NAEMylXhqqJuRqku6RSWbYxrIZuYPDbYqqarCAcR+0Eujd26L7U4puz2Y8dhO5ZwP7lu4xzrFrHZ524o0KUcx3muDzhU7dfuLt1pZGuMBnZt3+kzTv7SjlSr+yUChqzYo3uXCGtVWhO6UwG45/WtaZmv3dcWyIZXrLqtGGx8n80VgRJV79q5z5HdoiJ8neKiSdC8wBYUuhB3SosG3ilhHMeY11kkujmbppvFdionTOQLzTvGctHrH7G4KGjUNa0LoXVU5PaKqCSxL3aBi2Aq98Q1KoqHYDzXFluOQ0m8Igpd4q3NMLFR3/VuzHw4HCwFWATowDb0au9qYhftMpNDYZQ5UegEtgJ3OZU5YFqOsWns8o3cW5juWG7r2iIk/dXxkMvYZRoH926uH5EN34MyQofc+rUrvF72hZDSjiyG8UfOXiqJHWmqvdwg3/4U+QlrAKB+daqn7m4LY5MsVLlFORawbNaQI9NQi69tX/mRvtiV1pHoVlGRnPfLd022DXdh35au3fupVg8/3mFcl9MtIq4PxCfWQM5Y0Qs7eMVfNVqywqcuILCGW7dLoYc2h5VIb1Y4m8xxXrTxPqytCtyTrnJj+h+/LP93Gd5V3e08jpC2mraDNl1z1Z9cLBzjYidP/Df+abM4s+qHA1n2YnPotYohe9NtsEsZf9rRkUUhtciDQkjRLWFH5u6vWBvsB2kvlqDqcnGS7T+08c7hVtO+e3U7sDlw5zqYcCbrytAtX+V3kwhYm3jFakztAESWr2GuzsK1MjmrbNnyB7Xj4Mzr6Yqdxnf1iKBEl2WLbKIq/1n54/Pv9hmsgf5PXc61oUUObrLHXZrYPdeRfPxJy+ySgicLridnMHNyklm5HbAQOrQtl03njjcwmHOuKtYSNegaoP+qDLHr6GUltMJ0bR8REEtWwba6kHPl7rk1Uvz1By/dIuKawyFcsy2ldRBtU3Zhp8lsya5e+XX1npq75zXZWCFEiW5C3VrXtqEb7rgNQwizeTDewabkbZe0D0SVLaK2+zCWaeWJQEQwANb9JOoQPRT+yCVnhjqF26opbDbE2jSaxW2NdbqKCokLt3AjNdfzXLOxNokq1A7NEEwSq+T13KhdMY7Ndn86wXYoJFraAdr1V1d+V4wInShzV/XOzF2ZsdENYi4ienh4YOacs3oo1wo3b+RYmU040Q1JNbuayYntdOi/fG21EJ84ZX2X2Lc0orNHbn+ckxJrUG45C1u4g1qXayOAbOERTI6QuvdaTheLjoldLifMd6+7uzuduY0zkDDKtDjgdTzksjiWjV7I6jbqbfUFI3sMioUw1sbbV7R7bm1pxYJxz/Gy7bTZtUkO3JESbHarCGwY1fXIHzGwVe4WuGNp2O6Z+xnlfhzHx8dHO3kjq61x/rDbWziEHAiLnhZz3ZlMhD6ZtFW24n1r7ZsQBijoIdfKgTXkox6jop20HAjZNdiaWQWIGLJ63GHZ/f7jAGqF3Cnku0221+PjI61DNDRjdx0c1nDnEIPbnqx591nD9km5rsdk6bDfih6XgbYAh22LWuNZHo4Yj/dY9xJX7I8byUm4Y0hau9F9wWxRXdAoAbiHbkV8Sx73/Pb21p2thhaelvZqrGuL1bamiWFyFM+9MWJ/RbD1asYoFqqSORG7H8WKO0m62nFuTmWNCdDrjt2WYw2Ubsookm1mTBD7ZaQZGLR1rdgVmIOb2IG1XsMw3N/fx/a6KmwkpMtQNmU3QnLCOHbo1mi5qoa69juSHIaFO4Zxjib6uy6eLMy7xttqoS1ky6F0Wxuz2xK6m+YcgCIIbF073SZKGJ93e4tr+8PDg/3knopNRO4omS4vWFEtAuzVzW71aRFmO9J1vZ3M5YpwP+m9LchWFrnEFmtFpLUHcbm6ut6CRRQmVr2lpi5tRLU6SDk8xXLc5ezRlXwr483NjbozOw2N9qoGG/qxhdsRuwi5M08dtaQ1uuGeI2b/Pprqwu3o0Aw2v1WihRobWurisqtNBANHG2zpmgMfuHa6Yh1lutL2CaNrgy7Kt9rixIttdNc4jnd3d7EhEd9YG8vt+3PFRljbAl1XiWJbYa5vhtDandmcETH6a9f7dnETf6J1tKso3JoFiFcUWOW0CSzILL4dprFNhNqFeO3xnd7ZTB87nUTJf7CNRHR3d9dV6dbDqHOHJxvaWgaxpWkT3CqFlmlrv74Z0u099teulG4PtUtjjSc/lfBat/sztiraz7UzSmWfu+Y4UbvJ3OU6fezcW8qJzLSDnq2fjsej3S+riZ1R4tKHKl9Vaq1Z1m8XqT7lXod++/XqwxEbB45ofleiU5lrDAI4nHJdGvqB8X8XdrZ2y0DRGNb2scyd6vYvpR/bRtejXHdC6B5dAfQJEaWUbm5uuv0z2rJbkaMNFbL7gobaorsZMnZjzX6dtnbzRrxeLLN5XF/f0rur2D7RG63ChmKueYoVm8BiyJkTwSpRBV1RowDu2iLsfc6zPX6Hjbqy3d7eooXPtoQdDLkEvL6chLEQl9GmZ7MSZ0FWSlkdcpNScuusCD6lCx33k2o2UgWZrdkOFmxGH1FZERnu3mrKCY8NBHRV1mWLrauLWst/3futcrBW1PF4tIc3WFEtntw91tpzEroE1hVYfLjCo/bc9OnYbZ5tjC00vsvBa1bo2IzQDpjuh0TiNLu1w4SHXYPZAp06YoJoKpXf9QrXxlhFXh/nsANBh6RY2tYTIuqeMiuJ5X1ZbmsP8QSY7yohhj60fSBR7AYOdis2csROG5drrT50GeuN/IOVCa0BqPnTqGVLWqpQJ4N76KwVBe5mcR0pttFlt3m7DVf9ugTdnh3FI6LT6eTKt6Vp13Js1C3N3rv01MLtrUK2eq/rMO2z9us3eS041MbOTg5/sQErnQL1my/rGSZqPi5GfPS9xdouRFw7uQ1JHLacLrqa0mQ23tySyqmV1vHpfkNiveM42glrl91RYxfELiWtGcXhEus34mEQttVqJ4MPbMnE2lqfxr+0funM5rUxskUhFIggV5decqqwImme56enJw6eJUIZ647uGCLC0Zk2NqTb7Wy36a6AIvRap+X4cOsJGozc5JkrWf6t09a8umKx2pewBlDUDK1jGKsre+M0wLIXG4YAXMujLixQXEYBmY2fJBmv57u2eo+myTn/x3/8x3/+53++vr7KTL9tj23SVi933chqxymoix5bS1TFDrXsMGgXWF09y+FXZGKReOOE37q21BIv+7rSlniWnl05q/ONIlQj7mxK2xhah2DXakgOawEMvVHYKSFZxnGUQ3T+8pe/PDw8vL+/v7y8lFKGYTidTqJfN8+mFe2YsPvrFrb0uVOize70uGO57hWTqXhKybpVg4I/tX2DavC58lbOmpHCbXb7XGrMOcuNG1F1cSnVrc5ZctLY1LZWd6yO7foAKCXo2fTMYKYqTSKj8S2rp5Tu7+/v7+8LWBYjL5fL29vb77//nnM+nU43Nzc364+zbBW1BRF96FrtVMOGxmwC21ltw3ckiSLFyzmdqPZYVPxoItbWjLJFHxKlctxhdbLVqLGrlG531NIjDiwVEQD5LP2a1Zw0TjJrztovQaVN455uTh8+fLhcLu/v7+fzeZqm0ilhmAAAG/RJREFUT58+/aC6ozpsi7DGjft1izZkkiKu/W311/0y9RIqKu3QaiuYHYK4P3cusZSbC4zmsJaV+7I+CC+msS2COjU9g0JL3zoslszwzcIIa3NS+OKHJbAu2+mAqJYmUokwlGikYRhub29540tNP6JW1/itErQjOSERApQfqWifKfVPVZddm7KH8MNoz4XMW8UimAbBmjaL4lhfybWM1bWd/JRoDUO5usNvJ6vFcldlco4TtpHnkK4zaYp8IkpEcoCwzSXK3en637VxRIMtsAuULQ10JdmvCwHHzLwsy+VycVGXbt3ndmn2HVg7US1nO8p3zFTMy0ZOG7R+Z9DjDwHRO1fX8PqTu5TVNJmbFLD/rnSUEvRXku9TyUf0ruPBcRyJ6HK5nM/nZVlcE/Zb5KDvcqkkXWO4a9940RjdjMqvAOZ5jpOBVu2axd5jbVeX1+HASWtzOft2W63PLQGNrlyLd82zxTf6RH2ismIUVIvitX90RV3/lMECJTm5IiHx+szkeZ5fXl7+67/+a1mWh4eHn3766ePHjzc3N2n9NQwOocAOjW0l2HqItRlcepvL0YC97GnrzDzP8+FwsCqSJmuwwhuexQlm80Z5NCVtvF+LDaghGK7Ixx6iOWnD+zoMwoR7tHbbDjquhZBjTepnoK5fO0S337dibXvmeb5cLl+/fp2miZmfnp5eXl7+/ve/39zcPD4+Pj4+3t/fH49HK+0ORbke5prs/rV/RhfgjKG/ahZnS3nzFY2qJWXOWU/BUm7QMt16A3pW73ZX3o7NXQivbGT3k6k8TkUkS7OuOJXJKjcCKF7OU6juXKes7UH97muiVNgMeQgpJfm+ne1D9mzveZ7P57MEDbS+5nn+9u3b6+vr4XC4vb29v7+XCSdxgk54K9UOyLoQ0bZYTxH5xhq4y0MyT6YC6MyNRpYWf2oU1xYHICekhQVMn3FprB4shuyvtl32z75T6/KH/ZN6dI013smccrIqRO7LtaeuANqMYgePyRwdX0o5n8/SiT98+PD09CQ/De2SeyKapomIJHgax1HmnG5vb908u1ONU1xEAK8vp4q4qV71gPU1z7OL6uy9wovNwHnr6gLFGc7ayMJFq94xtAW6/VVrGe3TWOVWMxw4trLbUOnahpYHhETy0Wmj4gTZWOIMqftJBBZS5vF4/Ld/+7fff//9crloq4ZhEPrRCX4A8zxP0/T09CQ/3dzcnE6n4/EYR7YweIoGQCCV+NBhkY1TRguop2lyVGrVqH7N9jFngi4CumhzHKNPbDgLeIVjDTjnT2jtNFexkdWmhYLVlFZmp6dcP9ZCVFOOdQBQIub6UWiXS77nos/ZzKlcLhcdy0iCm5ubP//5zzpkk0HcOI72MDKnevmyzLdv30SVx+Px48ePwlLOPBFJ7rn7NSrKKlYacrlchITiuNUmjnObsdNak0d2ceGOk9/ZWrM7RoiFd1HViRjsz05fiuKoStdCJ9wK12BqHxixeSrtgZhQ1pYQKMvMimwZU/Hk/nQ6nU4ndTSlHbzEZnd6aQcn2DHBNE1//etf397ePnz48Oc///nTp093d3d2oOSAuNXq6OCwBpwIP88z1gfMd5EUgwFnJouALQayJrP3Wo7dAsRmqGRL3nKO7qEPsV37rTaVlmzkZTUV6U6vVeTORERsIomVMep36b3oOefz+RxZDQbcrihqJ0RbHDuqOBwODw8PpRTZmvL8/Hw8HiU8f3h4uL291cPwNeMW5cQ/SymCnvf3d411rLUiAr4LWdvGLSraCqQc5URcWr9m7bjFMnqNzhO5nDsqiz1DG++QpMLZnm1R2GU+V6ZgSHQkJrHluEIUQIqe0jsMSlJ++PCBW+QuERWA9/f3X375RXju9vb27u5O4GUItf/Vypzzsizv7+8SjdmBmF7OohbfUefdfm7lt+iJFOK6UASfJYgumGw5djOdZY2R1nRChoesmV0F0SrWnBGUKq5ziFZHNotjzsvlIoWL5WRXF4fjkbQECyY2oZX9Va+U0uPj4ziO8sk9mW2Sj4fmnF9eXoSotMA0DodhlC8Py6fTJGTW8qntuxK92e2dzur2CZmBLXpHGLpmuhtak0cXrK4EOye04xkdklwr5En/Q1gIvaHLLrGjuCdxaK0razH6dpvMVWiJiC0VOQG6nc91BiuMWwMWG9/e3t7e3pZSdD5Qta+VipMqsvAVToE9nU4S2osf1CXCLbu6G/ttoe4EgbORXtrPt8zhzE+NVyygnQ61XVbDsXxN39msaeu2gkbzdFvo6tY/Xexi6dHm1dbKvXR0pTGd6rUOS+sq68+p2vbLqrVymA26HcWqhPHdiarcUoVW+Usph8NB9gFLdfbT5rqlREaRNhmte6Yt0GnGws7ldfeu39KaeLpmdTxiTWALidjQ9KsQewvO7qaLJJfMSYCwwq8l6KWiW1vKN19dUVgTsutV1nXyemOoToXrUqiznBWewotEV+s2dMh/ZaZqMJdkeXt7k9l2IhIAyY1MhNo+oK1W2Dk9IGDI6UR7pl0It5hwC+RauNrFchIHhtvCmdyvnJqDhXsYAZTW72XbWmF6OQIzxRtrP0WVhKs2cnSXZFGasSCLnkuLFcXJgig3J6LMZIGldrUwki9eq7GHYbi5udEJTzXS09PT+XwWVCmGpBUyCy/H7zmAWkBbPSsU9IbNEN1FNiKGm10k48s0rwOTo7F9k9kO1lmadVfX3hZSNplL4KjIYiWWaZsq2pQ5Hqs41bKddBCCUadJ5sw5G2KzcXzyryzrsnFqqh1eexmtt6wxdDweZVJAXFVqr7i8vb1dLhdBj4WX7pSSZUGZa7B1WR7iNXPrZX2i07+d0nRs5HqyQ0+0hVWC1sXBycjD0WaLXOIMzL3ZIwsO/RcG1A5wCESFwHxqctVIMZtPaO1PbeyslEPGY1LYsCDqOx6Psjiqzsvuq1SKknuNsqWWw+GgyynUhmZqRVlbdRhS/cglU5EqJAwtYU0JDkMWAW7OUDub68CKLVssGwfSNVwEhnuohYwOBDGnPrTYj5erqYtuh/rIZxy+mmVpTNyQC32UbJRmrADFLO52WVCcUc5ZFkphzgeWS/+U9PKOiqBHYiA3LhMt3dzc6MqMJQ82sZoeN6MYUjTzei6HAgM5H+SU77DVhYUVyRUYjeLuHRLIblvDGjqWvrodOkKke9+FvEtc2jKFZlEZpCuz8V9avuN/CzgyM1sKOLRdxuoWpRypN6V0PB4lGlOKUkJS40WH0rWoHI42z/PYLpVZxp7MLPMLtjOw8WUu7tmqncwR9RZhcjmPERGgGW0y2qUl/dVm+c6aGgwbuaDMIsPmck5Kb1yZKpBFp22VWl2J+nA46ASSLU2Rwb3hBpurG61LFYotmdu0BrbWpeCjbZ+29x8+fJC9ddze/JIscqCsTIhb+rE1Um8CUy8bAFlVpPW2T2e7iAxrevtvhJTDkypZ2zuycWQRQ+7elqudxpVAaxq07YlgdaI7s3EbTgtQRGiLJEVbMhNLykPFnDNvi3UQ0Vo0TUyA700rdzuuDOK0BEkmTnCeZ51v5DDdkDYuWpMTerBwrKz3aT2aU+RFw2lplgJUtm5LvVOzjela3dVn6+hSkaIttsHJZ+/1km031F5rF1XKfiP71mVZr7GU9ateFhwqjMWKDuhgEO/GUGk93dLVj6UlhK2kmkCCvBi/K6Q09rIRuuMkSwZauNMz1jjburHd3gKRe2FT145j9wd38TriY+M7IpJsrvhTN7EqIq03dQhiRJUyeJaUwzDo9lNlKeUe3X6qf4okNo2VkA2lyXONlzWjJnNFWTLuUngcddtKHQ+JGKO5HBXJtRWfuarjjZMEAWQIUwAW/Q61tsDOvFHMsEVLLqXFGZsQxMLFlmbj5SicWlcWQ+TSUEkAp4GwWsW+H0jGx0UeUoHtRJRjLxjc608UfIfTqbOQNaf9dBXWvIu2y0/ODNVlkwigtB7qb/FEfO5Ecuawwne51hVuBx+jK2uHWpyCYEAa1R1LsFLKpUZ1tGRtibasRkTi3VSJwh8yXNdRlZ0L0MIdlzhhbI2OCyOG7J+2ZFtm14PbNlphdDAoN7K+azEklxrM0o/tww4N1t4OE1pCtF1XYFemjrRsspHWJnSWdvdW45FILSBcOXqpF3DmVEXYEjSZzOigOSmZsBFyEjAJCZW2Pq+hhno0e6X1zJNlrIgDB0Srlu92WfdEsW4BZAU7tMvBSDqPnn7hdEUhJLJ8w8Gl0PrkbvuvQ+RW66Jxr9OPWHcym9RBzRZkEyczI+DEsl0wkpzqwo62nOgSVssZIxoAcXNV6pIkEtc5Q72x/BThlcwUANYzUl2utX9GdUWLauHq1CyM0Lrl8XiU4Fph5HCjTOz4BqH7yaXDAte97XBhi7rsfewwjv9W29a6mkIgvSgur4mKgr/ABhax7gEWTGQ+iCOF5Jzf3991Lb0baUqIqrAo60UMO7JTaW2Yolrm9YVtJGHdc6J+pLpIRa5A5aHD4RDD6i2sdH9yNt0RzAnvkJTC7CVCvyJdU+sa214uT5TV4d3mTWYO2rbTQl51jRYma9xjnaAkvlwuGmvrth69gSF5AYT6OyUD56esbGW9zKKtcxMK9ldsdzOVXAGtpVkwSZM1JLKdRNsVUdWFVPffCIL9h9Y0tCYzXnsefb56E8oZO2rH6sVBtWx/yA09IDp1q2qSmWhOKR0OB3kHzV5qEtWvosp6RpjOKmASgOrFbXeA4sY2x2JFNetwxoGGae0smFnmJmwc7aIiIhIqsu7MRkUIfc9qbwtP7iaiwf7ZzWhzOfTIpbMkI9bHYtpuGo0dRYeZVVKd6r3VaYR2RJILFUspsgBS1puH1Gz6spGCSRdKo9fTP+3iiYLJPRHYaV06qJTyLTltdRg0uCtu7L0qmZnFkVkM6bu/qbceAjNSU3M4xXaVbB+mMDXqMtrO0C3BCuDZSLuy7Z20zS6OwyKu958jdBTlITH2OI7H4/F8PrtuZ8fkNuix5KT3zhIA7MuQvF4esd6HjTtjMwdrXa3rHq4cWeuNDGQL17DazjfGSSPXzSyGtO0RTN0nGqRHirKGpjU5RSTo89EB5bvAjERlpbFiRekt/Mt6bsbWNaxPQQBQSnl7e5MjRLsCYA0peS1VaUm7tf4bO7e1roDMOj6s4yFNaQeneqMyKIZk10Ax7wtoaTrZKBjamnJUM2HNCrYVK0AnIobNZRVlIwdbrDMc9VyTM4386d9TU7M5kGqJMbFLFh9Gw1ueswaACcmpBZgA7u7uSinPz8+yhdkygS0z3oghuyxFJoAlE5VrXuvmYFClyawYNnE2F5vYyI72ub0IIKMzDa7d6CGFqWrLSREfMNMBlOSdUiRKCvpITtjgDvskPlQvL3+OWFOILSsaaYcA7RV/7Wa3P7k2xF74+PiYUvr8+XNKSU6A1MsW6IZUMGZWjTg82cumsRRV2qEl0TGpDPKT7lWSS1bylZYs5sRf2/lGN2NkUe5MHvmDzbikaRMJBHSG7prddn5niC1UIFARuQONHQ7iky3cdLGiJcPMeltu66aUS2cXbawg+y5+++23z58/H49HYSasKWRfSLWipaguqrT5lh1LeEvJOj6NowU0ugNO2ciRqM4SOTZKYWDv6McByz5JZmbSXap2+6dVoDbQ2tGmUfV2QrSXl5cuyko43hvo04kDR8SEa0yEufvVPtEeLJYQq7y9vf36669fv35NKd3d3clBfWwcTSzflRz7WVrPGpBxqVh7Meue9N5GPxZGcdZRnshWbtmPq0iKDhe9zdSOSywmtprvGqs3dghsC+R19ELhhBMPypeXl651twARCSCt1+qcw9pC3nfbbBGsNrB2en9///r162+//fb6+ppSur+/18NAIu7322KFcdGJlcc6r2LW7+zasIVR3JgmCYjo9vZWYGTZaCu4dlixtGExEdsYe0vUM5nhDgy2otJcp1rhTNjIgcY5NcdJXa8ZPavcp/WUUuQwl2uLrthclgPkTZ2np6dv3749Pz/nnE+n08PDw93dXVpPh/5IZ3WSkwlyFUYW0PZGweQWztgE4JLm5ubm7u5OXw2IW4usACpwWi+lbd1Yp7ND/FEPP8Jk2Oh7lY2irnl9fGTsyhyGuxFMiutImztiwSF9HTs7StDR9bIs0zSdz+fn5+cvX768vb2Jy7NniXaV0sWuSmXp0F5uqY7bocT6pHs/TdPNzc39/f3t7a3GRspDgzlJ3PEQwn5qTdZV1w4nOXO4BM6BxAJdYlgYqYVilRFAO8aIDYh+xAnXJbb4q8OZVqSd3rKC7KV/fX39+vXr09OTHBF8f3//8PCQwopNlxH1iYJAK3J/lvU8uIuESpslyjlfLpfD4fDhwwc5MlDGaNTCIDsuU9yIDDY86sIdPaxgzQU7oLGGc2xiC+/CoJagbOQuC6xuKbR2ov/steW8XBoYAFlFRNRaqlCKki1v4vW+fPny/PxMRHJCrcz7cZi7t1VjvWXb3TvPxWH4pj8ty3I+n0+n08ePH+WoJKndxkMa6zhfpvp3gqkSaL1/CN9zTxy+QNJNHMHHPY9R719fX7s/+HTfmyVyf7JZaIuGdzdbpGoV52C0I4aylHN5SlESSMmeEzlVLQbmWpSjHwcae1M2JgLO5/PlcpED4BVDuvynm9HcxJVr5j6SugZynKo8p/pxauS1p7Oa3zL31SI2Nop5bKE/Qh47vLdVAgVXZUMux7c2S5fPbI90hrezgtM0XS6Xl5eXL1++fPv2jZlPp9Pj46P7xJaWo4jBOixz5OSQdD6fZQ3n06dPcgSgnoFkSQj2SynrrmIph3vujNch7I9oOLqRaB1HRfHyKZ+fn6P73JHpxx/CgNLB0Q0looK60u+n1OdWrXKV9WqGjrCEpeQU26enp69fv76/v9vAHL3geouQLPk9Pz9LJPTx40dBp47t3TQjNtxWt89EJWx17B0E7FzdbhlDlyjMNcRG+DZvt9zYmC16jOn327BfJrVYLa03k+ivW7msGBENxayhqtf79u3b09NTznkYBjn7UVaF0dxc5DmZd3h/f5cXq+/u7j58+PDw8CCTQ+5okbT7akfHZaxb4SIb2/bYb21eR+2xD3d7tXvIZjO71tUPsaM0aX3KgrVcvNcqS/gGjdPRFo0532TZyyXARo/c6kAqhkWDm0gUWMgnSuVrJPqJKnW4AkElNgWcDObtnFAKS8Iq3g5ioh72f9rq3hoVRUe2pX9rza4CO3K+vLw4nLqFhf12OrJx/cYli9l/5IoN2AJT7ExWoa6HWalcTMPrhXo5U1bCKXu+u5Qpk4duhdWxDrXLyrmFoS4lO3s7T+caa8vsdt2uVnc0v++gED9ZrNVsGdK1zT209UVQuyexi0R7I5jcPXdPInylwK7urGByDebgNnVYDmH2slU4rDirR56wQrrFTgu1Lly0hLgS5SLdtLGpKN47u7iMsetqpaSvO5b1C6yuDtcSW6Iyp32yRWZdGyN0hWLea+4afov5nL5K2Icf80a7OsOk9Y62WF1USLfYLXxbg1l1OeZwGnBZbFeB2QyUwgajmFiV3EWV3tiiYuIaG21BHr3L1qGut6smB+GdNFFTW4rm3SFuVJlN4LTgbtickoONL1ypwGJj+2dUlC3flkM9Gt5RVNRnN5d7HruQY8RYXaxC0RxJyJY8WvJ0md19xLUq2tUEw+pdLVuu3irfGTj27AiCLV1oShfyR5spFUW1qI9Qr7eT2FUamdWusxZzDrOtJRKMa2Dkjy6wLNE6CVVyrIGlgim1w7zwbkEpf47cg7lVRPypmJ2ECD1+y4QUvIyrxZUQleUIIKZEr2NF3oqJrTCqcSebhexWh07r9305vM+16RSMJm2WWJfe2Aaqafe9mGvvYM5aQei3CGt5TiqbcfOt2X0bu/Q2QUSD/BlnsZw5I1vGxttaYu9xGREwFGHXbbIr0DKQvbrNUcPbrmx1EvXpyMyaUFW3s+nFgkAFVr1ZKrIQ3+q6FtAucexUevnzjWyXdSXuGwxrA8TErhyrsi5Gt1jNFqjWtfCyD20TIpmhZxUrYeygNrsrygWhrvnWobgZVH3H3ILJqtH6RPvESmKzWGPbjC6mdLqlsMSrvzqqi9pYfSS0C7fYgeQqZv9lxL6jAadTlz6SmXMNViOuBFeLlcoK3xUjyrlFkFH73d7lcODybvXDKIwOtWxnjr2iu9+LTMiy1R+0KDZxmL2skBT2QyJ0p5EDDmLDtnpJvIncYO3R7dZdBrK2j7rAxuVs78rsIjvyU1ekWPVWt7Yyx06FHwCTPoxzQrZYWlOOPEnmOxaREZI5YMTOqig7OutrvbrPH2tTXkvo0k8sKNrG/iQ3duDWrdJVYR9uWc5p1okay7T3XcBZgeUmDuxj7Zb8uv3VPndMibX9Yr+3N3a7iBZu9wJY0Njsiqe08XKIa5otv3vZhisbucUcLdYf2hd7apQDGxaygHDgcMCi4BEo+CwnEq3JzElCa47tAjEyoqbvIilqQC7tx7FMV7JThT0XwAnZxaXjIYuebj90yXjtsHh91EJXOVogG7doa8Q6yLs+JONcbAaVqdtBHVq1RFeBRYkK4aCj91pdN4GVKsoTNaKy2SZs8VNsWuwMWpQmtuCz8/i2qKh9Vazt2TqHEhnLrs057+M6vL00jdZiJwVcIS6jNWUK7xFYNOuT1RDAsbGqbEuzWAPFzWh38efsahWhf+5vqtpCKtYoiaTl2uLg5SRBwE3Eh5VH3Za1KAJ6NLtbsYrOYquTREmw9qf2oculT1xs7mRzZWqWrefDMNDT0xPW5qQ1MXYV7US0NqPgd2wyCh7HPkSwULe0yB9Rzm7GLdmsVN1Bb8SNK402vEk3fVfg7o2lLi3KMaKVLSrNmcY+KeYMZ61uaynTtsJucKjdwJbuUBmhHRHgKsAGxu1leTLqxT53BNmlN82+RUtR4O6lklheib9uEZ61DfXeMd1qlIOOldYSQ1dXbKa8HRdq1V2es0+2nruH9j6Zd4tZQ+wthe7get8d2IZF1cQayfhBqxFVhIPvllRWNd1NB10u3CrZts4aKcrv2mXL3IL+1p/7eZUAbIu6eo5V2OeWTra6Pa2nizQ8T+aDf6W9PnVdXNxqzI6OttLHZLy+tlobt0y4ZI7/XFH2V9V4rCg+tA7ICWDF3pIKa6pwQmLdMy1hxD4TecXFMQjwsu2V55ELowewAbtcbqbAtsWCCev5i1q+04szsxMUG/2Sgge02lfMalMdVUQZnPaxntK17bQadOAGwKHYLe5EMM//K9QMkiiEQRiK3v/OunCGhpcwulJaCoSAv/0CE5f7tDJmu/W2pTdQwU8Wt8Wo73H4hwWd1sidb8cgb9JsS5GAqlNLSanE1SFHbCgUh8YfkQBPQ6Wd1/Fpkl6j2DpfJEQ0h161lR8810UQOKKLjjlWR36FotIW0iq67dc1VV3x9ENRLYnwXUFZOmNB/OQvJanbkk7AX9mxLCKmPept5rTMGoBGfmuAWnMeeI+CEAooMHQiRnyUWxjdHB62nsYhHL0iKPQbuKQRfZe/VVXyzXkBJzQH57yHvnAAAAAASUVORK5CYII=" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA005 TriSensor", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0102", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3} +OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/32/value/621281297/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 37, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 621281297, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/48/,{ "Instance": 1, "CommandClassId": 48, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/,{ "Label": "Sensor", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Binary Sensor State", "ValueIDKey": 625737744, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/281475602464786/,{ "Label": "Air Temperature", "Value": 20.700000762939454, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475602464786, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/844425555886098/,{ "Label": "Illuminance", "Value": 0.0, "Units": "Lux", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Luminance Sensor Value", "ValueIDKey": 844425555886098, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/1234567890/,{ "Label": "Relative Humidity", "Value": 56.7, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1234567890, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678901/,{ "Label": "Pressure", "Value": 123, "Units": "inHg", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Pressure Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/72057594672070676/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 37, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594672070676, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678902/,{ "Label": "Fake Power", "Value": 123, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 90, "Node": 37, "Genre": "User", "Help": "Power Sensor Value", "ValueIDKey": 12345678902, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678903/,{ "Label": "Fake Energy", "Value": 456, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 91, "Node": 37, "Genre": "User", "Help": "Energy Sensor Value", "ValueIDKey": 12345678903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678904/,{ "Label": "Fake Electric", "Value": 789, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 92, "Node": 37, "Genre": "User", "Help": "Electric Sensor Value", "ValueIDKey": 12345678904, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678905/,{ "Label": "Fake String", "Value": "fake", "Units": "", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Fake String Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/72620544625491988/,{ "Label": "Illuminance Units", "Value": { "List": [ { "Value": 1, "Label": "Lux" } ], "Selected": "Lux" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 258, "Node": 37, "Genre": "System", "Help": "Luminance Sensor Available Units", "ValueIDKey": 72620544625491988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/281475607691286/,{ "Label": "Motion Re-trigger Time", "Value": 30, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the delay time before PIR sensor can be triggered again to reset motion timeout counter. Value = 0 will disable PIR sensor from triggering until motion timeout has finished.", "ValueIDKey": 281475607691286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/562950584401942/,{ "Label": "Motion clear time", "Value": 240, "Units": "Second", "Min": 1, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 37, "Genre": "Config", "Help": "This configures the clear time when your motion sensor times out and sends a no motion status.", "ValueIDKey": 562950584401942, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/844425561112596/,{ "Label": "Motion Sensitivity", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" }, { "Value": 11, "Label": "11" } ], "Selected": "8" }, "Units": "", "Min": 0, "Max": 11, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the sensitivity that motion detect. 0 - PIR sensor disabled. 1 - Lowest sensitivity. 11 - Highest sensitivity.", "ValueIDKey": 844425561112596, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1125900537823252/,{ "Label": "Binary Sensor Report", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 37, "Genre": "Config", "Help": "Enable/disable sensor binary report when motion event is detected or cleared", "ValueIDKey": 1125900537823252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1407375514533908/,{ "Label": "Disable BASIC_SET to Associated nodes", "Value": { "List": [ { "Value": 0, "Label": "Disabled All Group Basic Set Command" }, { "Value": 1, "Label": "Enabled Group 2" }, { "Value": 2, "Label": "Enabled Group 3 " }, { "Value": 3, "Label": "Enabled Group 2 and Group 3" } ], "Selected": "Enabled Group 2 and Group 3" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the enabled or disabled send BASIC_SET command to nodes that associated in group 2 and group 3.", "ValueIDKey": 1407375514533908, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1688850491244564/,{ "Label": "Basic Set Value Settings for Group 2", "Value": { "List": [ { "Value": 0, "Label": "0xFF when motion is triggered and 0x00 when motion is cleared" }, { "Value": 1, "Label": "0x00 when motion is triggered and 0xFF when motion is cleared" }, { "Value": 2, "Label": "0xFF when motion is triggered" }, { "Value": 3, "Label": "0x00 when motion is triggered" }, { "Value": 4, "Label": "0x00 when motion event is cleared" }, { "Value": 5, "Label": "0xFF when motion event is cleared" } ], "Selected": "0xFF when motion is triggered and 0x00 when motion is cleared" }, "Units": "", "Min": 0, "Max": 5, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 37, "Genre": "Config", "Help": "Define Basic Set Value when motion event is triggered and / or cleared", "ValueIDKey": 1688850491244564, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1970325467955222/,{ "Label": "Temperature Alarm Value", "Value": 239, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the threshold value that alarm level for temperature. When the current ambient temperature value is larger than this configuration value, device will send a BASIC_SET = 0xFF to nodes associated in group 3. If current temperature value is less than this value, device will send a BASIC_SET = 0x00 to nodes associated in group 3. Value = [Value] x 0.1(Celsius / Fahrenheit) Available Settings: -400 to 850 (40.0 to 85.0 Celsius) or -400 to 1185 (-40.0 to 118.5 Fahrenheit). Default value: 239 (23.9 Celsius) or 750 (75.0 Fahrenheit)", "ValueIDKey": 1970325467955222, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/2814750398087188/,{ "Label": "LED over TriSensor", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 37, "Genre": "Config", "Help": "Enable or Disable LED over TriSensor This completely disables all LED reaction regardless of Parameter 9 - 13 settings", "ValueIDKey": 2814750398087188, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3096225374797844/,{ "Label": "Motion report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Green" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a motion report.", "ValueIDKey": 3096225374797844, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3377700351508500/,{ "Label": "Temperature report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a temperature report.", "ValueIDKey": 3377700351508500, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3659175328219156/,{ "Label": "Light report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a light report.", "ValueIDKey": 3659175328219156, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3940650304929812/,{ "Label": "Battery report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 37, "Genre": "Config", "Help": "It is possible to change the color of what the LED blinks when your TriSensor sends a battery report.", "ValueIDKey": 3940650304929812, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/4222125281640468/,{ "Label": "Wakeup report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a wakeup report.", "ValueIDKey": 4222125281640468, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Temperature Scale Setting", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "Configure temperature sensor scale type, Temperature to report in Celsius or Fahrenheit", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/5910975141904406/,{ "Label": "Temperature Threshold reporting", "Value": 20, "Units": "0.1", "Min": 0, "Max": 250, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in temperature to induce an automatic report for temperature sensor. Scale is identical setting in Parameter No.20. 0-> Disable Threshold Report for Temperature Sensor. Setting of value 20 can be a change of -2.0 or +2.0 (C or F depending on Parameter No.20) to induce automatic report or setting a value of 2 will be a change of 0.2(C or F). Available Settings: 0 to 250.", "ValueIDKey": 5910975141904406, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6192450118615062/,{ "Label": "Light intensity Threshold Value to Report", "Value": 100, "Units": "Lux", "Min": 0, "Max": 10000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 22, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in lux to induce an automatic report for light sensor.", "ValueIDKey": 6192450118615062, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6473925095325718/,{ "Label": "Temperature Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 23, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for temperature sensor report. This value is larger, the battery life is longer. And the temperature value changed is not obvious.", "ValueIDKey": 6473925095325718, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036374/,{ "Label": "Light Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for light sensor report. This value is larger, the battery life is longer. And the light intensity changed is not obvious.", "ValueIDKey": 6755400072036374, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300310/,{ "Label": "Temperature Offset Value", "Value": 0, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "The current measuring temperature value can be add and minus a value by this setting. The scale can be decided by Parameter Number 20. Temperature Offset Value = [Value] * 0.1(Celsius / Fahrenheit) Available Settings: -200 to 200.", "ValueIDKey": 8444249932300310, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010966/,{ "Label": "Light Intensity Offset Value", "Value": 0, "Units": "Lux", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "The current measuring light intensity value can be add and minus a value by this setting. Available Settings: -1000 to 1000.", "ValueIDKey": 8725724909010966, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/28147498302046230/,{ "Label": "Light Sensor Calibrated Coefficient", "Value": 1024, "Units": "", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 37, "Genre": "Config", "Help": "This configuration defines the calibrated scale for ambient light intensity. Because the method and position that the sensor mounted and the cover of sensor will bring measurement error, user can get more real light intensity by this parameter setting. User should run the steps as blows for calibrating 1) Set this parameter value to default (Assumes the sensor has been added in a Z- Wave Network). 2) Place a digital light meter close to sensor and keep the same direction, monitor the light intensity value (Vm) which report to controller and record it. The same time user should record the value (Vs) of light meter. 3) The scale calibration formula: k = Vm / Vs. 4) The value of k is then multiplied by 1024 and rounded to the nearest whole number. 5) Set the value getting in 5) to this parameter, calibrate finished. For example, Vm = 300, Vs = 2600, then k = 2600 / 300 = 8.6667 k = 8.6667 * 1024 = 8874.7 => 8875. The parameter should be set to 8875.", "ValueIDKey": 28147498302046230, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/,{ "Label": "Home Security", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 8, "Label": "Motion Detected at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 7, "Node": 37, "Genre": "User", "Help": "Home Security Alerts", "ValueIDKey": 1970325463777300, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/128/value/627048465/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 627048465, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/281475612213267/,{ "Label": "Minimum Wake-up Interval", "Value": 1800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 37, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475612213267, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/562950588923923/,{ "Label": "Maximum Wake-up Interval", "Value": 64800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 37, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950588923923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/844425565634579/,{ "Label": "Default Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 37, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425565634579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/1125900542345235/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 37, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900542345235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/635502611/,{ "Label": "Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 37, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 635502611, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "2.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0", "1.1" ], "TimeStamp": 1579566891} +OpenZWave/1/node/37/association/2/,{ "Name": "BasicSet report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/37/association/3/,{ "Name": "Temperature Alarm report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAKAAAADICAIAAADgCn1NAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO19SZMcyZXe89gjcl9qRRWqUAC6G91cmi1rklpO1Cw2B8lMB5m2HyGT/gBNB+k/6DKj85gOEkcco9Eoo81CjprNmW6yiUYDXQCqClWoysp9z8hYXAdHOl66R2QV0ERmZHW9Q9pLD3cP9/f5e597LB4kDENCCKUUAADgWr9i+suka7mSolFKFz7KrvU36MFhGMK1XF1RAIAQwv9f61dMv+bgKy7XHHzF9WsOvuJyzcFXXL/m4Csu1xx8xfVrDr7ics3BV1y/5uArLtccfMX1aw6+4nLNwVdcv+bgKy7XHHzF9WsOvuJyzcFXXL/m4Csu1xx8xfUrzsG+7wdBEAQBpRTHKmUimqYpirLAFr5p0SABo+wr6lyCIBiNRt5EgiCACRsRQjgtsaDFFFaJpmm6ruu6bpqmYRiYzBLSx9f34CXlYN5sSuloIkEQcCDJRFg2ATPBEOwvVwzDMAzDcRxN0yKLL5GQJX2zAQBc1+33+8PhEAAURcGIRsKJD0UGAJw5DMMwDDVNs23bcRwexpPQ96vPwf1+v9freZ6nqiqHFmeIBO9lnyV3JEh4DZTSMAyZYppmOp3mDr1EskweDAD9fr/T6VBK2RRJxlWAVnZoHLR5ZBZ4mvsrnQhzaAHmJNjkinAwpdR13VarFQSBqqrY2zAZQxRaLCXSfQWL4DNin+auzGC2LCuTychhI5mSdA8GAEppq9UaDodCQOb+GhlyL0yJOyoMhUiYKaXpdNqyLLlU0vSkczBzXEopdlwcilk4xQhxePAoEdxd9r8LRwAeVWEYBkGg63o2m024Hyfag3u9XrfbZY7LGZc5ECEkkoOF7vGIHdHzaXq+DE4c5nAiAJDL5djgS47dsJ5EDmZNarVao9GIXWnCQRIAGLog+aVQA5M4oo0M45eEGaZdOZPJGIYRWefCJYkeTCltNpu+72PSxVQShwSdvq4OyOIzMMZeHufQQthnw4tj7Pt+Op02TTM5NkwuB1NKG40Gmy2zyAySfSMBjgtFnLNnkG7kcJEpHwcMfCgMQ8/zUqkUnnYlRJJ1P5j5Ll4LAVrnMBGod0ZU5DUIiixCVfjUnO+Fi2U4p6Iouq4PBgPXdRduQ7EvyeFgSmmr1RqPx5qmYe/h2PCcgifhSTUeE7hUZIgWZMYhPKoEhyaTyQGL1ZlMRtf1GVXNWZLCwQDQ6/UGg4Ewq+I5X7Y4KjjH4SdnxoMjLo8sPHLQyeUUoSxrA7s1mcvlknPtOikc7Lpuu91WVVUOziyDjByelMVxpFA2cnAI4wymYSPo4uUMJ4bJ+o3de87lcl/VIr8nSQQHh2HI1rvYXvK0CBfE6IIEJ4/b3OGwIrsvv/8vUCxLkdMFh8Yp7Lff78/ZhnH64jmYUS9bFMmGYyLYF5eNVOTwHkfkwl85gMM0AQtn5znxwokFasdxkkDGi38mazQacXRlR5nRdAEG/Hf2lIoQwqMFTLs1nczYeSXCOk2oB7MyHyKqqlJKB4MBC9Rfaw6mlDYaDRzfeDrEc6fQDSHqYuFGxzVomha5YKWTK1PsWZ/IUwvjD8+58Ihh9bDnBS62wpuURXIwAAwGA0DPY7BEBlgkNkIlVBI52+wwIDCuqqqGYSiKwmZMM0rh5kX+VRTF8zzhAtz8dW02Ob05nQ3z0WgkT2EunBZgxsW4Xj684wyRTgnTceLCUSJM3FgoYh10HIefZf52XiQHM/fFwx+mwZPNKhAt9/VI34WZMEdOxyKDP17UzmgbTDMxIURVVd/3wzBUFGVRdn45g4i04JvTKaWu6+LrfxhgbEq5EpBmRnGZcZ5Iqo5rm9AG4UTykOLpMD28FEVh1y+/iq2+ir4wDh4OhxjXOFeLDN0i8U6DLYAB08AI8TZS59n4AilycOBSQvjBNfi+L4yYeeqL4WDmvoKtIWo1IkdgAVFZ4kZMHLRCIn+gALcWn46gK+QguTWdvgzOTjoej9nNxPljvBgOZtNLVVUBiYxZZESlk9v+kSJclJAr5DDEtU0+Kc9D4+85CkMQ0zZzYgbwnO0M7NWV+XMDnjxHelKkXIiu4KO4tziRveGiIMEBNjIwcHuxGRN3Yhpz20qI1WxZjAf03Gy+mHeTPM/DrsasEOltM/ogS6Q/yRXC5MYAu5MGEz9jcuGVH84jAsYwPZKE3rGrdfjonDxYZqw3qjN0eUokU8rFL4Mxd195oFB0AVI4xHXf9yPbwyuPG38zUnhZHqWvPgcDwHg8xm9sclRmeycgqECCPDIG4GzY2+Quzz7vbHSF9giZ8enk9Dno814HA/KVSMtiSCBmIRtZeZyDxpW6fOTnMfmSTRWK8F/WcfmMb1Sf9zoYMx8+JHsJT6FIYNqa3Hfx3SHcw8hwPQPpSBHYRGiMXGdcJXH3MN6oPm8OZgQsizz6ZkROblyWR7gHJes8BYMUGatpFI8K1cZNDoRYTdEEmwl+IR3mZfN5c7Dv+wQJxEskBnIMlBc5GEVcYSSWcahf2DD5b2RBflIckL6iDRPNwThMXShyTMaK8NS0EMMFRY7wckEhRQ68kVE68oyCzgvy7s/N5vPm4BkA48gsmFi2OAsAAgAQhdns0+G/kWXjwH4NIej9jKvJwZRSdiUI4kUwLpGuLMIkMstnwTXI3iwEfEER6hHOKIjML3INkaUiJ5hvVJ8rB8/wAwGPyBQmeG8UnnM8HvObNpE1zzBupAUEtOTH59iNXiHqXEjh3AJzs/lcr0XPvowsGALr2C6GYch3KTC6giLgJ58r8rwYOUKIvDsHjboqfiHGciPftD5XDqYxlwwFiWs0oNgo1C+DOqMSPGJkip1dVo4BM4bmjK5dTQ5mQ57GrCsEuo20OAc4zsWxCHmINCnjA252XBW6EwctSENZOPS14GC4hERmwzEz8qgMHkx7jBA/5aCNTyE7MT/KFawLeeLaj7lmPjZfwLXoSInLM9tTIyNzpDdHZruwAQIqM/qFqSducMzo15vTF3AtGuIlEj+Mx2x05Qqx0SPrkRNl4GVfl5t9YYpQ1dXk4EihUW8AQ4xTzoCZ63ExFqYDrKDA9GiQ23kh9cbBLI+Pedp8AfeDZ5hAURR8qUt26NkBgGcTYgYGXshDoyZfOIVOc3bkfELOPKOz8knfqD7XdTCZzJOxNwgDnF+ikl8ekYtjiRsKQrp8RqF5GEIm+NJbZLVCM8hEZuSfm83nysGypWQz8afg+L0Enp8JX2tBjNBpiUwREoUm0cmWDJGDTDhR5Hkhfoi/ht2+ij5XDmZvcHAvvNAXGcDsnXmWzrd/hWkLYne5zADHRWQGZW3Dj+Rxj+QF+WUs3ItI98UDSHjU8Cva8zL6XDl4xm2GSI/kpbD7zrhAGAewMGKEgrgGoeUUXXoTjvLTCYEdDwWYRhcXv5ocDAgtmPYhbDiQ8GalWHrkDUcM8OVbxavF4YRKc7RIDuZxBaZhk0cMHnkXPnzye9fnvQ6WfQi3Js4FCSFsfxaYtizPIMcG7vQwbWI6ifP4dDzyC+ly5Rw/uXKMLh5tuD2RdzmvDgcDgKqq+KkGzF5YkYGnEwLDIRr7kBylBTeC+AWbEDmFVrGBhbkTTwsi/R63EAt+OPAKcjAAaJrmui63pmAL2UAw7X8QtXziVcmDA3uS0B4ZGJZZHhPydooMYHxS2b7CGGUikMKbs/PCOFjXdW5Huf9CKRkeOtnDht2glT0YoiyLkeMRCw8ybHQMOaYG3Az2hLPs9Lg9MD3ImPvOzc5cnzcHc2AweLhN8iEBY/YiF0jo4o2McBHMuMIhLEIbYLIiZ/sq4r5wDxbG04V/hXeT5qMv4N0kVVVnb3Eii2Avz/PY/sz8KCGEBf/IUjgFpJGOd+YSohxIBAyTh3V4Zvw7o/GUUnmszEFfwPvBhmEMh0PBjoJFYBoG4eh4PLZtW5g5C0hEFpcDcuTMiNdAJu9M4No8z6PTzB2p8xTeHU3T5mlnuhAOppSaptnv94XIIbtapLcxYTQsA0ziH5kAhCv+xZtQCvQMEw4W2sAA5imRkzW5zcLLNXOz+QLeD9Z1nbeAdV6YoQgKnRaYOLGAH4c2zvWF5SwGJtILGQezzQh4fv50H275DGHZ2DeXcPrcbB7LHG9OKKXNZvPBgwfMPwCZm2dgCr7eC9MdkOsUFDlRAEOgK5m9eKLg2XJOiq5o4l86mRVms9m9vb24PS/fqCxsv+iTkxMAEOYdIBEwno5hE8vuS6e/a8SNy3VeCa+BeyofanjPWWGDB+ziMqHgQSDkZLuE27a9EDsvZo8OAHAcZzAYYNPglnEPECgQJqGYCQ+wfKcxkIaIjAc+FyCwyfTOwRgnmCl4wMnjlRAi7MAyT31h+2Sl02m2muT3d2W3AEkwipgLheAZGbEv/BtXs/x3huDKmei6LuwnPk99Yftk6bpuGAbbe4ZKd+UEhGTACJowxw0F4ez0ojkR3kFHKHWhE/MiAsZhGLLg/Er2+T3qi9yrMpPJ8L2EIUowupHOdxmnn30IhwQBWlwQ00FkPXio4UigKIppmjL8c9MXtlclADiOQyaXf7lFJNNdLHH1z068sIjcMP4r3F6MK8vc98J2vlF9wd9sSKfTgqUuAzO2O41iZYiaaePil4kHkee6ZFk6mVuwvYQvtMOb0xe2XzSTTCbTbrcjYyOTSGvCq4ziuOFCpWtPQoiLzBnZSJwNDwi2MdYC/QcW/s0GVVUdx+F3iIUMgsjgybbGMX9G8dmzrRllORnzdDw6BXdPpVJy2Tnri+RgpudyOfwUjozNjKB9eX+FVwc1rjGR6RhyJrquyy+qz19f/HeTDMNg88w4epMljhdnoMvzyNx84VlmV4j/8pQwDJn7Lta2sHAOZpEkl8udn59HTosuAzkOTZH56fREjJ1oxmiYLTgM4spxiNY0DX9tFjd1zvriv5sEALZts3ul3MkEC8JM75whkdjLZ4l0a4FoZV0Q3s4wDNPptJB+GTu8CX3xHMwkm83GPU0XN0LleiKhAmlmBK++5sb5hYJytYQQvjpauG0Xz8FM0uk0kSbAl4FhRh58SIABtyHSlePcNPIsWKGULnzti/XFczBM4l4mk+l2u9wJYFoE1pS5Ng4SmW4jvVyuRKBqeYjIpmONT6VSC8eV64ngYKZnMplOpwPxIkMl9wrnnAE5LoVnSTwD1imao804KctgWRZ/+Pnyfb/6HEwpVVXVtu24uMolLoTGpV84JmSvFdJl7pAzw8R98fRK7uP89aRwMNPZVIu3j0dCATaOJT8k/8adZcaggZjRIHN23DDSNG0hz8bO0BPBwVw3TVPXdf7UcSRUgrnj5rRyemRgwENE0CMbOSORTq5Nvl7f35CeIA5mejqdZrcfeBNnz7mEQMp/MRHKned/BXRlB5VHVVxLAIDfHEyOPRPEwUxJpVIzgJkdY2dgJucn0kPRkcUjz4vTeftldJOgJ4uDAUBRFDbVAiSR5ubwyEBiqHARoU45p5Aof/o27owY4IXbEOvJ4mCYXCjAbxlhI8JMNp2RjU+DcTbBWQXY+C+O/DgdkKiqirccTo49E8fBAGDbdqvVimsxjbpRjy3Lq4okb0AIxfl0ZPqMFEop+2Z83NhaoJ44DmaNE170mCECHrJT4myyh8V5rZBfOKNwXg7wV+/71edgJpiGZ5hewAmzslxnXKIsOB2/2RBXs6Io7IWrxdotUk8cB8PEIXiUxhlo1CMyuDhBYZxKd5DINJXKLwtFumykEEQE7GXlhdstUk8iB8PEJ4RvlAhTa0CgysNU6FdcEZwue+qMBRs/RCcP1y3cbpF6EjmYCX9eHAu2vnxI+L2M4Kk1mZbZ9eBDbOORC/t1zcFTumEYQgSOw5Wnvyq6uLjs2UKT4lqrqqoQ6hOlJ5GDmY4n0kTiXRlFhoHs9EwE8o7Mg6El0xQukz1P57uFJMRugp5EDubGjdzUgkyz42y0IqfiOA8WoeyM4YIPcYATYjdBTyIHz7AdFxmVSO8U8giHLqzzwlKUUmHHrqTpyeVgQJMXwf/iUOSZI/1SKCInxsVhoQFCU+V9lhKlJ5eDAQBfPeDplNLIyBkZZmnU0zaRZxQEUz5BfCwUYe6bZBsml4NhYj4swlQLC7cyjVkBzxBeFvv9jHNx4fF54baK0xPNwTwAQkzM5MI2j5QDaWSsljNg5ULhFVK0O+HCbRWnJ5qDYdqJMQZ4EIxGoz//8//Js3meR+mLDGyXq/HY40IpZV8hB4AwDNnIiJS4JvEG8BnWHOzw2nqiOZi5iOd5cjpPoZR+/PHf67peqZxns5kf//gn6Uyq0+68//63P/n0N5l0ulqrFfL509PTtbW10WhECEmlHM/zb97cPjw80g3Dtqw/+IMf8LPLY4jrMszz3//5VfVEczAAsKWwbFmeEgTB4dHhP//BDz766FemaXz43X+Uy2bHY+/P/vR//Kf//B81TWu12h9//PG9e/e+8Y33fvyXf/lHf/gHhCgHBwefP/jiP/z7fwsAh4eHbPNLXi2ReFdOgUlEkak6UXqi13CAQjT3XcGJP/vsfqlYOnh6MPa8drvtOClNVSnQ9Y0NtspKp1KuO7ZtmxDodfsPv3gEhPiBXygUWG1ra2vyUgckUCPdd+H2WXoOjjQ9Ttnf3/+TP/njf/JP//Ef/9Efuq5bq9VubG22252bN7d/8pOfHhwc/u8f/cUHH3wHgACQ9967F9JwfX21XqsDpZ988umjR49+9KP/M6P+GekL2f/5VfVXW07MXyil1WoVX81nsxumBEFQq9XW19cBQFGU8/NqpVLx/eDmznapWDyvVp+fPN/Z2UmnU/V6I5vNqqpyenrWarVu396zLOvw6MgduXfv3jEMQ9i4kJ+In4sl8vhBKTUMg20UNH+zXF4WtlflJXUAqNfrdPqDNPwonvvgJyBZnri93plwLBUkZCJ0WnjD+HnDMHQcB7/lvXBbReqJXgczwZFQlvF47Pt+pVLpdDqVSiUIwzAMK5UKWwuNx2O2wwsFOhgOWZHBYOD7/ng8ZsXZ7tNhGJ6enjLTCDvHM5E9NfJ7SknTF7Bf9Kt6MP8YA5/iAvLdx0+eFguFR19+ubqyYlnWycmnt27d+uTT36Yc+86d28+enQyGg71bu57nffHFow8+eL9YLP7853+1u7szGAxu3bpVq9XOz8+/973vPn16cOPGjf39x4PBgBCiaWoY0mKx0O321tfX2OP4GGPhbxJsFT2Lxlbjx5Kj8+AcR3WNekMhxLZshSi9bk/TNEKgkM+3Ws2HDx9pmmboOptd5/M55talUrHT6fZ6vcePH5fLZUrp6elZEITNZqPRaFmWFQQBIVCt1iqVSj6fT6X2IhuQ5Pv8XF8CDh4MBsyr5HkWAIxGI0VRwjDUdT0IAvZ2/XA4Mk1jNBqpqup5vqaphJDRyNU0le9PTAjxfd80zTCkhqEriuK6Y8exx+Oxruue52maNhq5uq7JI4xxcKFQwNZcuK2iPRgSwBMzdJiE6DgP3t9/TAjZ2Fiv1+qra2udTqdUKlmWef/+5+zzSr7vFwqFbrdnGMZw2H/77bcPDg7X1lZt2z4+PlYU1bbter3+wQffefbsWSaTOTg4yOcL2WyGEEJpWCgULcvkZ+eGE9qTBFstMQcLhzANP378ZGVlRVGUer0+HLmapjLHUlW13e4EQTAaDVVVazTqq6urnuc1Go0wDAFIs9nyPL9er7CtI7rd7unpGaU0l8s9evRoZ+cmABQKhXq9trW1BZKwiLJw+1yoL8E62PO8VquFr3jQiQBAr9e3bYt/y4i9odvtdnu9XiaToZTqut5qtSzLdt1RGIaZTIZtAut5XrPZzGQyg8HAMMx0OmUYRqPR0HWd7fCsqmq/3+92e9lsBoAoCuFv77OZQTabXZhdLi1JvxYNaDWCHZdnu3//vuM4lmWNx2PD0Pf29nRdPzp65jhOo9EwTSudTjebrVptf3194/T0eblcTqdTtm2fnVUGgyGlUKvV2+32nTt7+Xyh1+tXq1XHSRFC8vnc9vbWgwcPNzbW2QSAjRg5iiTEVkvMwXHMRwgpl8u+77PPnhWLRVbcsqwwDIrFIruUnU6ndV1XVXV7ezuTSWez2W63WywWLMtUFGVtbVXTVEVRTNMwDKNYLKyurtVqNV3XxuNxvpAbj8eOY7PrZTIZJ8dWkXrSZ9GEkDAM6/U6X5OwQzzD0dEzQkg2m+n1ep7np1KpIAja7fY777wNk+kuTEd1IolwJQvnx6cjkyVlEASGYeB31RNiqwgP5o2GiSRQ50YXpjZMXNdVlFy/P1BVjX0wi+1lxC5VCt0GSTjM8qFIEcB+033/inrSORjbUcaAUloul4Ig0HV9c3PDNE2+zGWOyzYqZvPwbrfLbvryW5A05gXiywgOJ8mx1fJxMNcxGNihf/WrX6+trRJCVFV1Xdc0zV6vZ9s2+7ihqqqZTPr09PTuW29VqzXXdYPAVxTVsqx8PreyssIrnwE2bg83nBBIEqsn/X4w9l0MNlNc1y2XS5RSNslyHKdYLNq2bZqmZVmmabJ9bFOptKaqnucVCgVd11OplO/77CPEkVEB4oVTshDVE6snfR3MpNlsBkGAyRimL1iyQ8LdXPxtFDqZbfHMXFgp/gsT75RL8fYEQZBKpS6/DcECZQk4+PJMSWNeF5P/ziiF0Y0sxUM0rjY5thL0pN8PFgiPG13GD6fjzDJOcg1xWMqluCwLByf9WjT3J3ZPHpDr8Dw4MxOMN0WPdnAR4jOJ2hJLGAfYy9kX+ZJjnxn6cqyDTdOs1+v4IWTZRyNdMNLdYQIwIKSFM3IDyRkopXxLrITYZ4a+HBzMNilqNpscY8FBYRpsXAOd+Vg1xgkjyi+A4we1eIV7e3tcT4J9ZuiEzxITLkEQHB4esguKMLmOwX/j3FfYnFiGkIMnbMEkh33u1o7jlEqluXT69yDLwcEAwLcLx56EgzMHD3ePSiLve0Um16J5uhDwhXS8J2Vy7BOnLwcHAwCl1DCM/f19vOkJD9GzwRYShXT2F9+UjAzRLL9lWWtra2Q6widZXw4OZnqpVHJdt9Vq8WgJk/g8wRgAIlwcJIzZL+ZXdFOSAEzFZ54tDMO9vT1+6iTY5EJ9aTiYyWAw6PV6zKMohYn/hpRSoEDQNSmYrGcoDSkVeVpRWFh+EQxUVWGBgdUZhiEfM4SwAfGiwnK5vLDOv5Yswf1gDAyTbrfzN3/1s8ODp71eL5vNsJuDmWy2UCimUmld18MwAELS6Vy706nVau1Wq9vtHT87rFROLcu8e/etmzt7qXTatixN01WV+N7YsixFVYfDwXnl7Oz0dDgaOY5jGuZwOCqVS2/fe++dd77FwF64HV5JXxoOxpLJZA3DOK+en52era2vbW/fNDXdcdLbN3fX19cJUYhC2q328fGzp0+fPPj888Ojo9Pnp71eL+WkdENvtroHR882Nzbz+Tx7v2hvb+/mzo6TynRazWar1Wi2Ou12NpsNgqBSqXz/+9/PZfMTV06EHS6vK5ycMF0lXGetJ0Du3XubhWsCwAB4/vzkv/3X/zIajl50jyiEKApRCCGmad65u7e6WlaUSYRGIf1nP/vpn/3pfwfy0jygEMsysrkco2uQrq4shb4094MFHYACgd/d//zGjS0AQgEYTa6vb/zrf/PvbNtmrx5R+oKpCSGe5+3vP7FtO5fLvaxkIh9++D2iqAAEKEOZAkC322+32zB92WThfX8lfTnuB8s6k8nUicJkyqWq6re++W2W8UV+NCrYa2fM6SdVvtBKpfL29u6LqthcmgJRFEopQVUkoe+vpC8ZB/PpMQDDAuAFti+8GKYAffmPxStUnETkBYprBYAXs2eYMlFy7HAZfZnWwVjn0EwweNkvBgcio5ciJOGjE9NM3PVFoH5pMjrh/oX3/ZX05bgfLOsAMI0IzocyTeM8NUQm4AmJMInRDE/RxxPQ968FB2fzedtxCCFUgneSiU28UEjnOaa/+A4gHOSjgZ/45eEk9P1rwcG5bN4wDApACHB/Y+RKCUzcL+IiSZzgszBhRTVN01RNRU97JccOl9GXlYNPT09azRZQ+oIsKfLiF9MjoJR5MyUAhBBVVVVVNU3DNE3d0BWFKArRdc2yTABoNOphrVatVvr9biGfzaQd27ZSqfT6+sb27u2V1fUl5eBlXQez7e9UTdN1XdU0wzRCGjabDW/sarrOHn+3LHP31q3yykq73Th48vj8vBKGYb6Qv3fvvbfefi+fLwDQbrftDgf9frNePanVzhuNxnjsm6ZpO5ZpWMVSOV9c2d29u/D+vrau/vCHP0xOPLm83u22VlfLt3Z3CAnb7fpw2Ot1W7Xz03a7ORx03dGQUqobhmlaRFHYFpXdTtfzg3Q6Y5h2u9WuVCpnZ6eddlvVja2t3RtbO5lsodPtP39+1u70PC/Yf/y42+2ur29sb+8uvL+vrb8CSyVKwiD48svPn+w/evLk8Wg0KpfLlmWnM5kbN7Y3NjfT6bTv+/V64+nTJ/fv3//iiwfPnh23Wm1CSD6fW11dLZeKlm1TShWilErFzc1N0zRrterBwdNmsxGGoaoqKysrN2/uFEsrW9u79+59Q9eNRXf6dWRZOZgoytr6jd98+g+V8/N+v396duZ7nm07W9tbGxsbtm27I7fZalTPz8/Pzwf9tqGTQt7RVM12TAW8Qb/te0NNU3XdGAy08wpVVLXf71Ma6Lo+dsdhQHu9/snJydnZ+Wg4XCmvrm/cSErfvw4cDADV8zNVgZVyAag3Hnu6aqgqHQ76zUa9b5h+EPQHw7EfeH7gusFg4A5HI1VRvQBUzUqlrUyukMmkLcsulUq3925v39xRNbV6Xnn06OHJyXEYBNlMxrKsVCq9c+t2sVROTt9fSV+m+8FY/+2nv/7oo18cHh5qmp5KpwzDWFtdv3P37trqmm4YQRC47rjb7dYb9Ua9VqtVK2en9XqNEJLL5XZv7b51914+X9tCjMoAAAgXSURBVFA1bdDrjL0x0GA4GjYa9Vr1fNAfGKZpGIZl2Sur65ub2996/0NClDfXlzfrwWQ518HDYX9tbd2xrV6v47qurkGvU/vtb5rpVDpfyJdKK5lMvlDIl1fKo+HO6emxQuho2Pf9wDQNx0m743G701EIUVQ1k82nHIcoSi5fI6CfjI9HI09VlYPDk053uLK2ScgS7IcVpy8rB3/ng+89fPi7s9MT3/c9L3TstJNKpdLp8srqSnnVSTm+H7TbrWfHzx7vP3568PT05LTVbvu+n0qljp6dra6u5vM5TdMURS2XS5ubW6ahn52dPtr/slKp0DBUVbVUKhUL+UatdvB0f2f3NveEhff9a8HBhmlt37z1+f3PPvvd/eFwlM1mfd93HGd399bmjSabZDUajcp55fT5Se280u93wmBs6JqpK2Hgdlq1Yb+laqpt24S6EI6BQK1WHfTaKgkDoIHvtdvN01Oz1x+MRkPbcdbWNhPS91fSl+a5aEEHgF6vZxrGzs0btWrV833LVIB6tepzbzzQdX3s+YP+oN1pd7td13U9z/f9IAypq3tmQImq207Gtm3dMHQzXSyv7+3dSaXTzUb9wYP7BwdPwzAsFgqmaRmGsba2nsnkktP3V9KXch1MKX3y+OGvfvXLp0+esC12AEh5ZeXtt9+5eXMnk8mGNOx1u9Va9fnJydlZpdGo16rVbrejKEo2k9na2tre3V1dWbMdO/C9sTtSVWXsjprNZq1W7fd77CFLXTPW1jc2t3a+/e0PNd3A9LZEsqwcfHJ8FPhBsVgkQAGoqiqqSg+fPqycHqbTmfLKaj5f3Lm589bdd9zx6PDp44/+3y/2v3wUhtQ01dJKcXNjM5vNE0J830unMoRQP/BB0VwvCALi+75lpwaDQbPV3d41NN2ASdBLQt+/Fhz8/nc+dFJOo16tnp93Oh1NUwzTsu1UvlAoFkupVJpSenJycnZ2dnR0eHJyUqtWO91e4Aftntvpuk+eHBWLRdtxbMu6cWNrd/eWYRj93vDs7Pzp0ydAqa7rqZSTSqVq1Uq9Xi2VVl6vnQvXl3UdDACNevVnP/3x3330d77nb2xsAoBt27fv3N3e3rYsa9AfnFerx8fPDg6eHj87bjQao9FI1/VsNpPNZm3bUhRCKTV0vVgqrq2t2ZbVajXPzk57vT6llFJqGObW9vbqynomm/3u9/9ZqbSakL6/mgfz2T83XPJ11vQgCCzbur27WzmvtJpVVVWGAz0M3Wb9jG1F2el0641Gv9cOQ09ViaGrikrCMPB9LwxNy7RM0zRM00ll0pnCja2te06q2+18+eWXR0eHvu8VC0VKSbVWK6+saJrOm5EcO1xGX1YOrp6f/frjXx4eHrQ7HU0ziaLn8rm9vds7O7eKxaKqqoPBoFqrHh0dqrpNFEPTW67rqqqaSadXV1c2b2yvra2xB98BQk3VPG9cPX/eabc0ld7a2dI0zTCMbC5fLK28++77Tip9zcFz1R88+Ozo6HAwGK2urqdSDgFQFMV1hw8ffBaEgWXZ5fLK5ub2rd1brus+3n/497/+uFI5I0Bsx966sfHNb31zc+umqqrDQb/f746GA8/3Aj+koBCiBhRUzfADOhqNTct2UumF9/e19WVdB7/19ruqQtzRqNNt93q9IKQqgK7ohm1Ztp0vFHL54nA0Oj45OTk5Pjg8OK9URqMxpdT16ZePD6r1Vj6fd2zbSaVu3ty5e/fdVMo5PDj4m7/96y++eEBDapmm73vbN2+ms7nhcGDbTnL6/kr6sq6DAeDs9ORv//r//sMnn/iet7W1pel6KpW6feetnZ0dx3H6/cHZ2emTJ08ePny4v79fqVSGw6Gu6/l8rpAv2I4dUhr4vqHr5XJpbW3dcezhcNhoNJqNOpuOlcvlVCq9urr2rfe/8+5772N6WyJZVg4GANOyPN/3PK/X6x6fHKuqms/n05mMZZmpVNr3vfHY1TQtm82wLQsHgwEh4DhOOpPe2NjY2NjM5/O242Sz2VKxpOuq67rn5+eVs+e9Xl/XtVwuZ9uO46RK5VVY2nXwkr0fjGUw6P/yFz/vdjr8E7GU0iAMXNcNg/Dee9945533bDtFCBmP3d999ulf/Oh/ddpt07K2t7f/xb/8V7u37qiqCkDCMGg2aw8f/G40GsHkdRhVVUzTMgw9X1i5+9a7C+3oV5JlXQczvdNuPXjw2+Gg77put9tp1Bv1er1er7fa7dFoaOh6Kp3RNW08HnvemBCFzY0Nw3AcO5PJZDJZXTcohAohhmFalmXZtmmapmlomk4UZWVlfWf3jrD2SEjfL6kvJQczYS0PguDs9LhRr/b73eFg2B/0+/3+oN8fjkbeeOxP3vAnhBBCXrzTr6qaqmqapum6ruuGYZgTMYwX/9KZ7MrqRjqdXVLq5bLcHox7Qik9PX1erdZUVSGEEJjsvcARmkKKwOQdM/qiNKUUFEXZ29sTdhlNQh+/jhwcKc+fP+90OnwnHiY4gzAymP4CXoCtra2l2EP28rKs7ybF6RsbG+zDOYJ/Y7AFnee5ceMG/prowvvye9GXmIMjhfni8fHxcDjEfixk437M8odhuLGxkfyPAb+GXAUOlvUwDM/Pz48OnxBFMU3TtmxKaavd6vf6pmVmM1nTsoDSTqcDQPP54vrGJvui1ku7JKYvX1G/ahyMxfe9Qb8/cofjset7vh94NAiJQlT1xfRZNwzHTpmWffUcl8vV9OBr/aUHXzEOvhZBlvVa9LV+Sf0qc/C1AMD/B04ffJuL1wCiAAAAAElFTkSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} +OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} +OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} +OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/fixtures/ozw/light.json b/tests/fixtures/ozw/light.json new file mode 100644 index 00000000000..81fd82a4a2b --- /dev/null +++ b/tests/fixtures/ozw/light.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/", + "payload": { + "Label": "Level", + "Value": 0, + "Units": "", + "Min": 0, + "Max": 255, + "Type": "Byte", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", + "Index": 0, + "Node": 39, + "Genre": "User", + "Help": "The Current Level of the Device", + "ValueIDKey": 659128337, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} diff --git a/tests/fixtures/ozw/light_network_dump.csv b/tests/fixtures/ozw/light_network_dump.csv new file mode 100644 index 00000000000..2a2329a1c39 --- /dev/null +++ b/tests/fixtures/ozw/light_network_dump.csv @@ -0,0 +1,55 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAKAAAADICAIAAADgCn1NAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO19SZMcyZXe89gjcl9qRRWqUAC6G91cmi1rklpO1Cw2B8lMB5m2HyGT/gBNB+k/6DKj85gOEkcco9Eoo81CjprNmW6yiUYDXQCqClWoysp9z8hYXAdHOl66R2QV0ERmZHW9Q9pLD3cP9/f5e597LB4kDENCCKUUAADgWr9i+suka7mSolFKFz7KrvU36MFhGMK1XF1RAIAQwv9f61dMv+bgKy7XHHzF9WsOvuJyzcFXXL/m4Csu1xx8xfVrDr7ics3BV1y/5uArLtccfMX1aw6+4nLNwVdcv+bgKy7XHHzF9WsOvuJyzcFXXL/m4Csu1xx8xfUrzsG+7wdBEAQBpRTHKmUimqYpirLAFr5p0SABo+wr6lyCIBiNRt5EgiCACRsRQjgtsaDFFFaJpmm6ruu6bpqmYRiYzBLSx9f34CXlYN5sSuloIkEQcCDJRFg2ATPBEOwvVwzDMAzDcRxN0yKLL5GQJX2zAQBc1+33+8PhEAAURcGIRsKJD0UGAJw5DMMwDDVNs23bcRwexpPQ96vPwf1+v9freZ6nqiqHFmeIBO9lnyV3JEh4DZTSMAyZYppmOp3mDr1EskweDAD9fr/T6VBK2RRJxlWAVnZoHLR5ZBZ4mvsrnQhzaAHmJNjkinAwpdR13VarFQSBqqrY2zAZQxRaLCXSfQWL4DNin+auzGC2LCuTychhI5mSdA8GAEppq9UaDodCQOb+GhlyL0yJOyoMhUiYKaXpdNqyLLlU0vSkczBzXEopdlwcilk4xQhxePAoEdxd9r8LRwAeVWEYBkGg63o2m024Hyfag3u9XrfbZY7LGZc5ECEkkoOF7vGIHdHzaXq+DE4c5nAiAJDL5djgS47dsJ5EDmZNarVao9GIXWnCQRIAGLog+aVQA5M4oo0M45eEGaZdOZPJGIYRWefCJYkeTCltNpu+72PSxVQShwSdvq4OyOIzMMZeHufQQthnw4tj7Pt+Op02TTM5NkwuB1NKG40Gmy2zyAySfSMBjgtFnLNnkG7kcJEpHwcMfCgMQ8/zUqkUnnYlRJJ1P5j5Ll4LAVrnMBGod0ZU5DUIiixCVfjUnO+Fi2U4p6Iouq4PBgPXdRduQ7EvyeFgSmmr1RqPx5qmYe/h2PCcgifhSTUeE7hUZIgWZMYhPKoEhyaTyQGL1ZlMRtf1GVXNWZLCwQDQ6/UGg4Ewq+I5X7Y4KjjH4SdnxoMjLo8sPHLQyeUUoSxrA7s1mcvlknPtOikc7Lpuu91WVVUOziyDjByelMVxpFA2cnAI4wymYSPo4uUMJ4bJ+o3de87lcl/VIr8nSQQHh2HI1rvYXvK0CBfE6IIEJ4/b3OGwIrsvv/8vUCxLkdMFh8Yp7Lff78/ZhnH64jmYUS9bFMmGYyLYF5eNVOTwHkfkwl85gMM0AQtn5znxwokFasdxkkDGi38mazQacXRlR5nRdAEG/Hf2lIoQwqMFTLs1nczYeSXCOk2oB7MyHyKqqlJKB4MBC9Rfaw6mlDYaDRzfeDrEc6fQDSHqYuFGxzVomha5YKWTK1PsWZ/IUwvjD8+58Ihh9bDnBS62wpuURXIwAAwGA0DPY7BEBlgkNkIlVBI52+wwIDCuqqqGYSiKwmZMM0rh5kX+VRTF8zzhAtz8dW02Ob05nQ3z0WgkT2EunBZgxsW4Xj684wyRTgnTceLCUSJM3FgoYh10HIefZf52XiQHM/fFwx+mwZPNKhAt9/VI34WZMEdOxyKDP17UzmgbTDMxIURVVd/3wzBUFGVRdn45g4i04JvTKaWu6+LrfxhgbEq5EpBmRnGZcZ5Iqo5rm9AG4UTykOLpMD28FEVh1y+/iq2+ir4wDh4OhxjXOFeLDN0i8U6DLYAB08AI8TZS59n4AilycOBSQvjBNfi+L4yYeeqL4WDmvoKtIWo1IkdgAVFZ4kZMHLRCIn+gALcWn46gK+QguTWdvgzOTjoej9nNxPljvBgOZtNLVVUBiYxZZESlk9v+kSJclJAr5DDEtU0+Kc9D4+85CkMQ0zZzYgbwnO0M7NWV+XMDnjxHelKkXIiu4KO4tziRveGiIMEBNjIwcHuxGRN3Yhpz20qI1WxZjAf03Gy+mHeTPM/DrsasEOltM/ogS6Q/yRXC5MYAu5MGEz9jcuGVH84jAsYwPZKE3rGrdfjonDxYZqw3qjN0eUokU8rFL4Mxd195oFB0AVI4xHXf9yPbwyuPG38zUnhZHqWvPgcDwHg8xm9sclRmeycgqECCPDIG4GzY2+Quzz7vbHSF9giZ8enk9Dno814HA/KVSMtiSCBmIRtZeZyDxpW6fOTnMfmSTRWK8F/WcfmMb1Sf9zoYMx8+JHsJT6FIYNqa3Hfx3SHcw8hwPQPpSBHYRGiMXGdcJXH3MN6oPm8OZgQsizz6ZkROblyWR7gHJes8BYMUGatpFI8K1cZNDoRYTdEEmwl+IR3mZfN5c7Dv+wQJxEskBnIMlBc5GEVcYSSWcahf2DD5b2RBflIckL6iDRPNwThMXShyTMaK8NS0EMMFRY7wckEhRQ68kVE68oyCzgvy7s/N5vPm4BkA48gsmFi2OAsAAgAQhdns0+G/kWXjwH4NIej9jKvJwZRSdiUI4kUwLpGuLMIkMstnwTXI3iwEfEER6hHOKIjML3INkaUiJ5hvVJ8rB8/wAwGPyBQmeG8UnnM8HvObNpE1zzBupAUEtOTH59iNXiHqXEjh3AJzs/lcr0XPvowsGALr2C6GYch3KTC6giLgJ58r8rwYOUKIvDsHjboqfiHGciPftD5XDqYxlwwFiWs0oNgo1C+DOqMSPGJkip1dVo4BM4bmjK5dTQ5mQ57GrCsEuo20OAc4zsWxCHmINCnjA252XBW6EwctSENZOPS14GC4hERmwzEz8qgMHkx7jBA/5aCNTyE7MT/KFawLeeLaj7lmPjZfwLXoSInLM9tTIyNzpDdHZruwAQIqM/qFqSducMzo15vTF3AtGuIlEj+Mx2x05Qqx0SPrkRNl4GVfl5t9YYpQ1dXk4EihUW8AQ4xTzoCZ63ExFqYDrKDA9GiQ23kh9cbBLI+Pedp8AfeDZ5hAURR8qUt26NkBgGcTYgYGXshDoyZfOIVOc3bkfELOPKOz8knfqD7XdTCZzJOxNwgDnF+ikl8ekYtjiRsKQrp8RqF5GEIm+NJbZLVCM8hEZuSfm83nysGypWQz8afg+L0Enp8JX2tBjNBpiUwREoUm0cmWDJGDTDhR5Hkhfoi/ht2+ij5XDmZvcHAvvNAXGcDsnXmWzrd/hWkLYne5zADHRWQGZW3Dj+Rxj+QF+WUs3ItI98UDSHjU8Cva8zL6XDl4xm2GSI/kpbD7zrhAGAewMGKEgrgGoeUUXXoTjvLTCYEdDwWYRhcXv5ocDAgtmPYhbDiQ8GalWHrkDUcM8OVbxavF4YRKc7RIDuZxBaZhk0cMHnkXPnzye9fnvQ6WfQi3Js4FCSFsfxaYtizPIMcG7vQwbWI6ifP4dDzyC+ly5Rw/uXKMLh5tuD2RdzmvDgcDgKqq+KkGzF5YkYGnEwLDIRr7kBylBTeC+AWbEDmFVrGBhbkTTwsi/R63EAt+OPAKcjAAaJrmui63pmAL2UAw7X8QtXziVcmDA3uS0B4ZGJZZHhPydooMYHxS2b7CGGUikMKbs/PCOFjXdW5Huf9CKRkeOtnDht2glT0YoiyLkeMRCw8ybHQMOaYG3Az2hLPs9Lg9MD3ImPvOzc5cnzcHc2AweLhN8iEBY/YiF0jo4o2McBHMuMIhLEIbYLIiZ/sq4r5wDxbG04V/hXeT5qMv4N0kVVVnb3Eii2Avz/PY/sz8KCGEBf/IUjgFpJGOd+YSohxIBAyTh3V4Zvw7o/GUUnmszEFfwPvBhmEMh0PBjoJFYBoG4eh4PLZtW5g5C0hEFpcDcuTMiNdAJu9M4No8z6PTzB2p8xTeHU3T5mlnuhAOppSaptnv94XIIbtapLcxYTQsA0ziH5kAhCv+xZtQCvQMEw4W2sAA5imRkzW5zcLLNXOz+QLeD9Z1nbeAdV6YoQgKnRaYOLGAH4c2zvWF5SwGJtILGQezzQh4fv50H275DGHZ2DeXcPrcbB7LHG9OKKXNZvPBgwfMPwCZm2dgCr7eC9MdkOsUFDlRAEOgK5m9eKLg2XJOiq5o4l86mRVms9m9vb24PS/fqCxsv+iTkxMAEOYdIBEwno5hE8vuS6e/a8SNy3VeCa+BeyofanjPWWGDB+ziMqHgQSDkZLuE27a9EDsvZo8OAHAcZzAYYNPglnEPECgQJqGYCQ+wfKcxkIaIjAc+FyCwyfTOwRgnmCl4wMnjlRAi7MAyT31h+2Sl02m2muT3d2W3AEkwipgLheAZGbEv/BtXs/x3huDKmei6LuwnPk99Yftk6bpuGAbbe4ZKd+UEhGTACJowxw0F4ez0ojkR3kFHKHWhE/MiAsZhGLLg/Er2+T3qi9yrMpPJ8L2EIUowupHOdxmnn30IhwQBWlwQ00FkPXio4UigKIppmjL8c9MXtlclADiOQyaXf7lFJNNdLHH1z068sIjcMP4r3F6MK8vc98J2vlF9wd9sSKfTgqUuAzO2O41iZYiaaePil4kHkee6ZFk6mVuwvYQvtMOb0xe2XzSTTCbTbrcjYyOTSGvCq4ziuOFCpWtPQoiLzBnZSJwNDwi2MdYC/QcW/s0GVVUdx+F3iIUMgsjgybbGMX9G8dmzrRllORnzdDw6BXdPpVJy2Tnri+RgpudyOfwUjozNjKB9eX+FVwc1rjGR6RhyJrquyy+qz19f/HeTDMNg88w4epMljhdnoMvzyNx84VlmV4j/8pQwDJn7Lta2sHAOZpEkl8udn59HTosuAzkOTZH56fREjJ1oxmiYLTgM4spxiNY0DX9tFjd1zvriv5sEALZts3ul3MkEC8JM75whkdjLZ4l0a4FoZV0Q3s4wDNPptJB+GTu8CX3xHMwkm83GPU0XN0LleiKhAmlmBK++5sb5hYJytYQQvjpauG0Xz8FM0uk0kSbAl4FhRh58SIABtyHSlePcNPIsWKGULnzti/XFczBM4l4mk+l2u9wJYFoE1pS5Ng4SmW4jvVyuRKBqeYjIpmONT6VSC8eV64ngYKZnMplOpwPxIkMl9wrnnAE5LoVnSTwD1imao804KctgWRZ/+Pnyfb/6HEwpVVXVtu24uMolLoTGpV84JmSvFdJl7pAzw8R98fRK7uP89aRwMNPZVIu3j0dCATaOJT8k/8adZcaggZjRIHN23DDSNG0hz8bO0BPBwVw3TVPXdf7UcSRUgrnj5rRyemRgwENE0CMbOSORTq5Nvl7f35CeIA5mejqdZrcfeBNnz7mEQMp/MRHKned/BXRlB5VHVVxLAIDfHEyOPRPEwUxJpVIzgJkdY2dgJucn0kPRkcUjz4vTeftldJOgJ4uDAUBRFDbVAiSR5ubwyEBiqHARoU45p5Aof/o27owY4IXbEOvJ4mCYXCjAbxlhI8JMNp2RjU+DcTbBWQXY+C+O/DgdkKiqirccTo49E8fBAGDbdqvVimsxjbpRjy3Lq4okb0AIxfl0ZPqMFEop+2Z83NhaoJ44DmaNE170mCECHrJT4myyh8V5rZBfOKNwXg7wV+/71edgJpiGZ5hewAmzslxnXKIsOB2/2RBXs6Io7IWrxdotUk8cB8PEIXiUxhlo1CMyuDhBYZxKd5DINJXKLwtFumykEEQE7GXlhdstUk8iB8PEJ4RvlAhTa0CgysNU6FdcEZwue+qMBRs/RCcP1y3cbpF6EjmYCX9eHAu2vnxI+L2M4Kk1mZbZ9eBDbOORC/t1zcFTumEYQgSOw5Wnvyq6uLjs2UKT4lqrqqoQ6hOlJ5GDmY4n0kTiXRlFhoHs9EwE8o7Mg6El0xQukz1P57uFJMRugp5EDubGjdzUgkyz42y0IqfiOA8WoeyM4YIPcYATYjdBTyIHz7AdFxmVSO8U8giHLqzzwlKUUmHHrqTpyeVgQJMXwf/iUOSZI/1SKCInxsVhoQFCU+V9lhKlJ5eDAQBfPeDplNLIyBkZZmnU0zaRZxQEUz5BfCwUYe6bZBsml4NhYj4swlQLC7cyjVkBzxBeFvv9jHNx4fF54baK0xPNwTwAQkzM5MI2j5QDaWSsljNg5ULhFVK0O+HCbRWnJ5qDYdqJMQZ4EIxGoz//8//Js3meR+mLDGyXq/HY40IpZV8hB4AwDNnIiJS4JvEG8BnWHOzw2nqiOZi5iOd5cjpPoZR+/PHf67peqZxns5kf//gn6Uyq0+68//63P/n0N5l0ulqrFfL509PTtbW10WhECEmlHM/zb97cPjw80g3Dtqw/+IMf8LPLY4jrMszz3//5VfVEczAAsKWwbFmeEgTB4dHhP//BDz766FemaXz43X+Uy2bHY+/P/vR//Kf//B81TWu12h9//PG9e/e+8Y33fvyXf/lHf/gHhCgHBwefP/jiP/z7fwsAh4eHbPNLXi2ReFdOgUlEkak6UXqi13CAQjT3XcGJP/vsfqlYOnh6MPa8drvtOClNVSnQ9Y0NtspKp1KuO7ZtmxDodfsPv3gEhPiBXygUWG1ra2vyUgckUCPdd+H2WXoOjjQ9Ttnf3/+TP/njf/JP//Ef/9Efuq5bq9VubG22252bN7d/8pOfHhwc/u8f/cUHH3wHgACQ9967F9JwfX21XqsDpZ988umjR49+9KP/M6P+GekL2f/5VfVXW07MXyil1WoVX81nsxumBEFQq9XW19cBQFGU8/NqpVLx/eDmznapWDyvVp+fPN/Z2UmnU/V6I5vNqqpyenrWarVu396zLOvw6MgduXfv3jEMQ9i4kJ+In4sl8vhBKTUMg20UNH+zXF4WtlflJXUAqNfrdPqDNPwonvvgJyBZnri93plwLBUkZCJ0WnjD+HnDMHQcB7/lvXBbReqJXgczwZFQlvF47Pt+pVLpdDqVSiUIwzAMK5UKWwuNx2O2wwsFOhgOWZHBYOD7/ng8ZsXZ7tNhGJ6enjLTCDvHM5E9NfJ7SknTF7Bf9Kt6MP8YA5/iAvLdx0+eFguFR19+ubqyYlnWycmnt27d+uTT36Yc+86d28+enQyGg71bu57nffHFow8+eL9YLP7853+1u7szGAxu3bpVq9XOz8+/973vPn16cOPGjf39x4PBgBCiaWoY0mKx0O321tfX2OP4GGPhbxJsFT2Lxlbjx5Kj8+AcR3WNekMhxLZshSi9bk/TNEKgkM+3Ws2HDx9pmmboOptd5/M55talUrHT6fZ6vcePH5fLZUrp6elZEITNZqPRaFmWFQQBIVCt1iqVSj6fT6X2IhuQ5Pv8XF8CDh4MBsyr5HkWAIxGI0VRwjDUdT0IAvZ2/XA4Mk1jNBqpqup5vqaphJDRyNU0le9PTAjxfd80zTCkhqEriuK6Y8exx+Oxruue52maNhq5uq7JI4xxcKFQwNZcuK2iPRgSwBMzdJiE6DgP3t9/TAjZ2Fiv1+qra2udTqdUKlmWef/+5+zzSr7vFwqFbrdnGMZw2H/77bcPDg7X1lZt2z4+PlYU1bbter3+wQffefbsWSaTOTg4yOcL2WyGEEJpWCgULcvkZ+eGE9qTBFstMQcLhzANP378ZGVlRVGUer0+HLmapjLHUlW13e4EQTAaDVVVazTqq6urnuc1Go0wDAFIs9nyPL9er7CtI7rd7unpGaU0l8s9evRoZ+cmABQKhXq9trW1BZKwiLJw+1yoL8E62PO8VquFr3jQiQBAr9e3bYt/y4i9odvtdnu9XiaToZTqut5qtSzLdt1RGIaZTIZtAut5XrPZzGQyg8HAMMx0OmUYRqPR0HWd7fCsqmq/3+92e9lsBoAoCuFv77OZQTabXZhdLi1JvxYNaDWCHZdnu3//vuM4lmWNx2PD0Pf29nRdPzp65jhOo9EwTSudTjebrVptf3194/T0eblcTqdTtm2fnVUGgyGlUKvV2+32nTt7+Xyh1+tXq1XHSRFC8vnc9vbWgwcPNzbW2QSAjRg5iiTEVkvMwXHMRwgpl8u+77PPnhWLRVbcsqwwDIrFIruUnU6ndV1XVXV7ezuTSWez2W63WywWLMtUFGVtbVXTVEVRTNMwDKNYLKyurtVqNV3XxuNxvpAbj8eOY7PrZTIZJ8dWkXrSZ9GEkDAM6/U6X5OwQzzD0dEzQkg2m+n1ep7np1KpIAja7fY777wNk+kuTEd1IolwJQvnx6cjkyVlEASGYeB31RNiqwgP5o2GiSRQ50YXpjZMXNdVlFy/P1BVjX0wi+1lxC5VCt0GSTjM8qFIEcB+033/inrSORjbUcaAUloul4Ig0HV9c3PDNE2+zGWOyzYqZvPwbrfLbvryW5A05gXiywgOJ8mx1fJxMNcxGNihf/WrX6+trRJCVFV1Xdc0zV6vZ9s2+7ihqqqZTPr09PTuW29VqzXXdYPAVxTVsqx8PreyssIrnwE2bg83nBBIEqsn/X4w9l0MNlNc1y2XS5RSNslyHKdYLNq2bZqmZVmmabJ9bFOptKaqnucVCgVd11OplO/77CPEkVEB4oVTshDVE6snfR3MpNlsBkGAyRimL1iyQ8LdXPxtFDqZbfHMXFgp/gsT75RL8fYEQZBKpS6/DcECZQk4+PJMSWNeF5P/ziiF0Y0sxUM0rjY5thL0pN8PFgiPG13GD6fjzDJOcg1xWMqluCwLByf9WjT3J3ZPHpDr8Dw4MxOMN0WPdnAR4jOJ2hJLGAfYy9kX+ZJjnxn6cqyDTdOs1+v4IWTZRyNdMNLdYQIwIKSFM3IDyRkopXxLrITYZ4a+HBzMNilqNpscY8FBYRpsXAOd+Vg1xgkjyi+A4we1eIV7e3tcT4J9ZuiEzxITLkEQHB4esguKMLmOwX/j3FfYnFiGkIMnbMEkh33u1o7jlEqluXT69yDLwcEAwLcLx56EgzMHD3ePSiLve0Um16J5uhDwhXS8J2Vy7BOnLwcHAwCl1DCM/f19vOkJD9GzwRYShXT2F9+UjAzRLL9lWWtra2Q6widZXw4OZnqpVHJdt9Vq8WgJk/g8wRgAIlwcJIzZL+ZXdFOSAEzFZ54tDMO9vT1+6iTY5EJ9aTiYyWAw6PV6zKMohYn/hpRSoEDQNSmYrGcoDSkVeVpRWFh+EQxUVWGBgdUZhiEfM4SwAfGiwnK5vLDOv5Yswf1gDAyTbrfzN3/1s8ODp71eL5vNsJuDmWy2UCimUmld18MwAELS6Vy706nVau1Wq9vtHT87rFROLcu8e/etmzt7qXTatixN01WV+N7YsixFVYfDwXnl7Oz0dDgaOY5jGuZwOCqVS2/fe++dd77FwF64HV5JXxoOxpLJZA3DOK+en52era2vbW/fNDXdcdLbN3fX19cJUYhC2q328fGzp0+fPPj888Ojo9Pnp71eL+WkdENvtroHR882Nzbz+Tx7v2hvb+/mzo6TynRazWar1Wi2Ou12NpsNgqBSqXz/+9/PZfMTV06EHS6vK5ycMF0lXGetJ0Du3XubhWsCwAB4/vzkv/3X/zIajl50jyiEKApRCCGmad65u7e6WlaUSYRGIf1nP/vpn/3pfwfy0jygEMsysrkco2uQrq4shb4094MFHYACgd/d//zGjS0AQgEYTa6vb/zrf/PvbNtmrx5R+oKpCSGe5+3vP7FtO5fLvaxkIh9++D2iqAAEKEOZAkC322+32zB92WThfX8lfTnuB8s6k8nUicJkyqWq6re++W2W8UV+NCrYa2fM6SdVvtBKpfL29u6LqthcmgJRFEopQVUkoe+vpC8ZB/PpMQDDAuAFti+8GKYAffmPxStUnETkBYprBYAXs2eYMlFy7HAZfZnWwVjn0EwweNkvBgcio5ciJOGjE9NM3PVFoH5pMjrh/oX3/ZX05bgfLOsAMI0IzocyTeM8NUQm4AmJMInRDE/RxxPQ968FB2fzedtxCCFUgneSiU28UEjnOaa/+A4gHOSjgZ/45eEk9P1rwcG5bN4wDApACHB/Y+RKCUzcL+IiSZzgszBhRTVN01RNRU97JccOl9GXlYNPT09azRZQ+oIsKfLiF9MjoJR5MyUAhBBVVVVVNU3DNE3d0BWFKArRdc2yTABoNOphrVatVvr9biGfzaQd27ZSqfT6+sb27u2V1fUl5eBlXQez7e9UTdN1XdU0wzRCGjabDW/sarrOHn+3LHP31q3yykq73Th48vj8vBKGYb6Qv3fvvbfefi+fLwDQbrftDgf9frNePanVzhuNxnjsm6ZpO5ZpWMVSOV9c2d29u/D+vrau/vCHP0xOPLm83u22VlfLt3Z3CAnb7fpw2Ot1W7Xz03a7ORx03dGQUqobhmlaRFHYFpXdTtfzg3Q6Y5h2u9WuVCpnZ6eddlvVja2t3RtbO5lsodPtP39+1u70PC/Yf/y42+2ur29sb+8uvL+vrb8CSyVKwiD48svPn+w/evLk8Wg0KpfLlmWnM5kbN7Y3NjfT6bTv+/V64+nTJ/fv3//iiwfPnh23Wm1CSD6fW11dLZeKlm1TShWilErFzc1N0zRrterBwdNmsxGGoaoqKysrN2/uFEsrW9u79+59Q9eNRXf6dWRZOZgoytr6jd98+g+V8/N+v396duZ7nm07W9tbGxsbtm27I7fZalTPz8/Pzwf9tqGTQt7RVM12TAW8Qb/te0NNU3XdGAy08wpVVLXf71Ma6Lo+dsdhQHu9/snJydnZ+Wg4XCmvrm/cSErfvw4cDADV8zNVgZVyAag3Hnu6aqgqHQ76zUa9b5h+EPQHw7EfeH7gusFg4A5HI1VRvQBUzUqlrUyukMmkLcsulUq3925v39xRNbV6Xnn06OHJyXEYBNlMxrKsVCq9c+t2sVROTt9fSV+m+8FY/+2nv/7oo18cHh5qmp5KpwzDWFtdv3P37trqmm4YQRC47rjb7dYb9Ua9VqtVK2en9XqNEJLL5XZv7b51914+X9tCjMoAAAgXSURBVFA1bdDrjL0x0GA4GjYa9Vr1fNAfGKZpGIZl2Sur65ub2996/0NClDfXlzfrwWQ518HDYX9tbd2xrV6v47qurkGvU/vtb5rpVDpfyJdKK5lMvlDIl1fKo+HO6emxQuho2Pf9wDQNx0m743G701EIUVQ1k82nHIcoSi5fI6CfjI9HI09VlYPDk053uLK2ScgS7IcVpy8rB3/ng+89fPi7s9MT3/c9L3TstJNKpdLp8srqSnnVSTm+H7TbrWfHzx7vP3568PT05LTVbvu+n0qljp6dra6u5vM5TdMURS2XS5ubW6ahn52dPtr/slKp0DBUVbVUKhUL+UatdvB0f2f3NveEhff9a8HBhmlt37z1+f3PPvvd/eFwlM1mfd93HGd399bmjSabZDUajcp55fT5Se280u93wmBs6JqpK2Hgdlq1Yb+laqpt24S6EI6BQK1WHfTaKgkDoIHvtdvN01Oz1x+MRkPbcdbWNhPS91fSl+a5aEEHgF6vZxrGzs0btWrV833LVIB6tepzbzzQdX3s+YP+oN1pd7td13U9z/f9IAypq3tmQImq207Gtm3dMHQzXSyv7+3dSaXTzUb9wYP7BwdPwzAsFgqmaRmGsba2nsnkktP3V9KXch1MKX3y+OGvfvXLp0+esC12AEh5ZeXtt9+5eXMnk8mGNOx1u9Va9fnJydlZpdGo16rVbrejKEo2k9na2tre3V1dWbMdO/C9sTtSVWXsjprNZq1W7fd77CFLXTPW1jc2t3a+/e0PNd3A9LZEsqwcfHJ8FPhBsVgkQAGoqiqqSg+fPqycHqbTmfLKaj5f3Lm589bdd9zx6PDp44/+3y/2v3wUhtQ01dJKcXNjM5vNE0J830unMoRQP/BB0VwvCALi+75lpwaDQbPV3d41NN2ASdBLQt+/Fhz8/nc+dFJOo16tnp93Oh1NUwzTsu1UvlAoFkupVJpSenJycnZ2dnR0eHJyUqtWO91e4Aftntvpuk+eHBWLRdtxbMu6cWNrd/eWYRj93vDs7Pzp0ydAqa7rqZSTSqVq1Uq9Xi2VVl6vnQvXl3UdDACNevVnP/3x3330d77nb2xsAoBt27fv3N3e3rYsa9AfnFerx8fPDg6eHj87bjQao9FI1/VsNpPNZm3bUhRCKTV0vVgqrq2t2ZbVajXPzk57vT6llFJqGObW9vbqynomm/3u9/9ZqbSakL6/mgfz2T83XPJ11vQgCCzbur27WzmvtJpVVVWGAz0M3Wb9jG1F2el0641Gv9cOQ09ViaGrikrCMPB9LwxNy7RM0zRM00ll0pnCja2te06q2+18+eWXR0eHvu8VC0VKSbVWK6+saJrOm5EcO1xGX1YOrp6f/frjXx4eHrQ7HU0ziaLn8rm9vds7O7eKxaKqqoPBoFqrHh0dqrpNFEPTW67rqqqaSadXV1c2b2yvra2xB98BQk3VPG9cPX/eabc0ld7a2dI0zTCMbC5fLK28++77Tip9zcFz1R88+Ozo6HAwGK2urqdSDgFQFMV1hw8ffBaEgWXZ5fLK5ub2rd1brus+3n/497/+uFI5I0Bsx966sfHNb31zc+umqqrDQb/f746GA8/3Aj+koBCiBhRUzfADOhqNTct2UumF9/e19WVdB7/19ruqQtzRqNNt93q9IKQqgK7ohm1Ztp0vFHL54nA0Oj45OTk5Pjg8OK9URqMxpdT16ZePD6r1Vj6fd2zbSaVu3ty5e/fdVMo5PDj4m7/96y++eEBDapmm73vbN2+ms7nhcGDbTnL6/kr6sq6DAeDs9ORv//r//sMnn/iet7W1pel6KpW6feetnZ0dx3H6/cHZ2emTJ08ePny4v79fqVSGw6Gu6/l8rpAv2I4dUhr4vqHr5XJpbW3dcezhcNhoNJqNOpuOlcvlVCq9urr2rfe/8+5772N6WyJZVg4GANOyPN/3PK/X6x6fHKuqms/n05mMZZmpVNr3vfHY1TQtm82wLQsHgwEh4DhOOpPe2NjY2NjM5/O242Sz2VKxpOuq67rn5+eVs+e9Xl/XtVwuZ9uO46RK5VVY2nXwkr0fjGUw6P/yFz/vdjr8E7GU0iAMXNcNg/Dee9945533bDtFCBmP3d999ulf/Oh/ddpt07K2t7f/xb/8V7u37qiqCkDCMGg2aw8f/G40GsHkdRhVVUzTMgw9X1i5+9a7C+3oV5JlXQczvdNuPXjw2+Gg77put9tp1Bv1er1er7fa7dFoaOh6Kp3RNW08HnvemBCFzY0Nw3AcO5PJZDJZXTcohAohhmFalmXZtmmapmlomk4UZWVlfWf3jrD2SEjfL6kvJQczYS0PguDs9LhRr/b73eFg2B/0+/3+oN8fjkbeeOxP3vAnhBBCXrzTr6qaqmqapum6ruuGYZgTMYwX/9KZ7MrqRjqdXVLq5bLcHox7Qik9PX1erdZUVSGEEJjsvcARmkKKwOQdM/qiNKUUFEXZ29sTdhlNQh+/jhwcKc+fP+90OnwnHiY4gzAymP4CXoCtra2l2EP28rKs7ybF6RsbG+zDOYJ/Y7AFnee5ceMG/prowvvye9GXmIMjhfni8fHxcDjEfixk437M8odhuLGxkfyPAb+GXAUOlvUwDM/Pz48OnxBFMU3TtmxKaavd6vf6pmVmM1nTsoDSTqcDQPP54vrGJvui1ku7JKYvX1G/ahyMxfe9Qb8/cofjset7vh94NAiJQlT1xfRZNwzHTpmWffUcl8vV9OBr/aUHXzEOvhZBlvVa9LV+Sf0qc/C1AMD/B04ffJuL1wCiAAAAAElFTkSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} +OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} +OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} +OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/fixtures/ozw/sensor.json b/tests/fixtures/ozw/sensor.json new file mode 100644 index 00000000000..17b86f90809 --- /dev/null +++ b/tests/fixtures/ozw/sensor.json @@ -0,0 +1,38 @@ +{ + "topic": "OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/", + "payload": { + "Label": "Instance 1: Water", + "Value": { + "List": [ + { + "Value": 0, + "Label": "Clear" + }, + { + "Value": 2, + "Label": "Water Leak at Unknown Location" + } + ], + "Selected": "Clear", + "Selected_id": 0 + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_NOTIFICATION", + "Index": 5, + "Node": 36, + "Genre": "User", + "Help": "Water Alerts", + "ValueIDKey": 1407375493578772, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} \ No newline at end of file diff --git a/tests/fixtures/ozw/switch.json b/tests/fixtures/ozw/switch.json new file mode 100644 index 00000000000..0d3fc37e9b2 --- /dev/null +++ b/tests/fixtures/ozw/switch.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/", + "payload": { + "Label": "Switch", + "Value": false, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "Bool", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", + "Index": 0, + "Node": 32, + "Genre": "User", + "Help": "Turn On/Off Device", + "ValueIDKey": 541671440, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} diff --git a/tests/fixtures/roku/roku3-device-info-power-off.xml b/tests/fixtures/roku/roku3-device-info-power-off.xml new file mode 100644 index 00000000000..4a89724016b --- /dev/null +++ b/tests/fixtures/roku/roku3-device-info-power-off.xml @@ -0,0 +1,35 @@ + + + 015e5108-9000-1046-8035-b0a737964dfb + 1GU48T017973 + 1GU48T017973 + Roku + 4200X + Roku 3 + US + true + b0:a7:37:96:4d:fb + b0:a7:37:96:4d:fa + ethernet + My Roku 3 + 7.5.0 + 09021 + true + en + US + en_US + US/Pacific + -480 + PowerOff + false + false + false + true + 70f6ed9c90cf60718a26f3a7c3e5af1c3ec29558 + true + true + true + false + false + false + diff --git a/tests/fixtures/roku/rokutv-device-info-power-off.xml b/tests/fixtures/roku/rokutv-device-info-power-off.xml new file mode 100644 index 00000000000..658fc130629 --- /dev/null +++ b/tests/fixtures/roku/rokutv-device-info-power-off.xml @@ -0,0 +1,72 @@ + + + 015e5555-9000-5555-5555-b0a555555dfb + YN00H5555555 + 0S596H055555 + 055555a9-d82b-5c75-b8fe-5555550cb7ee + Onn + 100005844 + 7820X + US + true + false + 58 + 2 + ATSC + true + d8:13:99:f8:b0:c6 + realtek + d4:3a:2e:07:fd:cb + wifi + NetworkSSID + 58" Onn Roku TV + Onn Roku TV + Onn Roku TV - YN00H5555555 + 58" Onn Roku TV + Living room + AT9.20E04502A + 9.2.0 + 4502 + true + en + US + en_US + true + US/Central + United States/Central + America/Chicago + -300 + 12-hour + 264789 + PowerOn + true + true + false + true + true + false + + true + true + true + true + false + true + true + true + false + 0.9 + true + true + true + true + true + https://www.onntvsupport.com/ + 2.9.57 + 3.0 + 2.9.42 + 2.8.20 + false + true + true + diff --git a/tests/fixtures/roku/rokutv-tv-active-channel.xml b/tests/fixtures/roku/rokutv-tv-active-channel.xml new file mode 100644 index 00000000000..9d6bf582726 --- /dev/null +++ b/tests/fixtures/roku/rokutv-tv-active-channel.xml @@ -0,0 +1,24 @@ + + + + 14.3 + getTV + air-digital + false + true + valid + 480i + 20 + -75 + Airwolf + The team will travel all around the world in order to shut down a global crime ring. + TV-14-D-V + none + stereo + eng + AC3 + eng + AC3 + true + + diff --git a/tests/fixtures/roku/rokutv-tv-channels.xml b/tests/fixtures/roku/rokutv-tv-channels.xml new file mode 100644 index 00000000000..db4b816c9e2 --- /dev/null +++ b/tests/fixtures/roku/rokutv-tv-channels.xml @@ -0,0 +1,15 @@ + + + + 1.1 + WhatsOn + air-digital + false + + + 1.3 + QVC + air-digital + false + + diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index aabc71ca9d8..1b5121d75e0 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -8,9 +8,11 @@ from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.aiohttp_client as client from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch -@pytest.fixture -def camera_client(hass, hass_client): + +@pytest.fixture(name="camera_client") +def camera_client_fixture(hass, hass_client): """Fixture to fetch camera streams.""" assert hass.loop.run_until_complete( async_setup_component( @@ -91,6 +93,82 @@ async def test_get_clientsession_cleanup_without_ssl(hass): assert hass.data[client.DATA_CONNECTOR_NOTVERIFY].closed +async def test_get_clientsession_patched_close(hass): + """Test closing clientsession does not work.""" + with patch("aiohttp.ClientSession.close") as mock_close: + session = client.async_get_clientsession(hass) + + assert isinstance(hass.data[client.DATA_CLIENTSESSION], aiohttp.ClientSession) + assert isinstance(hass.data[client.DATA_CONNECTOR], aiohttp.TCPConnector) + + with pytest.raises(RuntimeError): + await session.close() + + assert mock_close.call_count == 0 + + +async def test_warning_close_session_integration(hass, caplog): + """Test log warning message when closing the session from integration context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + session = client.async_get_clientsession(hass) + await session.close() + assert ( + "Detected integration that closes the Home Assistant aiohttp session. " + "Please report issue for hue using this method at " + "homeassistant/components/hue/light.py, line 23: await session.close()" + ) in caplog.text + + +async def test_warning_close_session_custom(hass, caplog): + """Test log warning message when closing the session from custom context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + session = client.async_get_clientsession(hass) + await session.close() + assert ( + "Detected integration that closes the Home Assistant aiohttp session. " + "Please report issue to the custom component author for hue using this method at " + "custom_components/hue/light.py, line 23: await session.close()" in caplog.text + ) + + async def test_async_aiohttp_proxy_stream(aioclient_mock, camera_client): """Test that it fetches the given url.""" aioclient_mock.get("http://example.com/mjpeg_stream", content=b"Frame1Frame2Frame3") diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 87e4b4c4d03..f6a75fe3c30 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,12 +1,12 @@ """Tests for the Area Registry.""" import asyncio -import asynctest import pytest from homeassistant.core import callback from homeassistant.helpers import area_registry +import tests.async_mock from tests.common import flush_store, mock_area_registry @@ -166,7 +166,7 @@ async def test_loading_area_from_storage(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.helpers.area_registry.AreaRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 7a5d606bcc8..ffc05544694 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config helper.""" import logging -from unittest.mock import patch from homeassistant.config import YAML_CONFIG_FILE from homeassistant.helpers.check_config import ( @@ -8,6 +7,7 @@ from homeassistant.helpers.check_config import ( async_check_ha_config_file, ) +from tests.async_mock import patch from tests.common import patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 4e8e59aad78..c4b87b667fa 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,9 +1,30 @@ """Test the condition helper.""" -from unittest.mock import patch +import pytest +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition from homeassistant.util import dt +from tests.async_mock import patch + + +async def test_invalid_condition(hass): + """Test if invalid condition raises.""" + with pytest.raises(HomeAssistantError): + await condition.async_from_config( + hass, + { + "condition": "invalid", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + ], + }, + ) + async def test_and_condition(hass): """Test the 'and' condition.""" @@ -127,6 +148,73 @@ async def test_or_condition_with_template(hass): assert test(hass) +async def test_not_condition(hass): + """Test the 'not' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "not", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 50, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 101) + assert test(hass) + + hass.states.async_set("sensor.temperature", 50) + assert test(hass) + + hass.states.async_set("sensor.temperature", 49) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert not test(hass) + + +async def test_not_condition_with_template(hass): + """Test the 'or' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "not", + "conditions": [ + { + "condition": "template", + "value_template": '{{ states.sensor.temperature.state == "100" }}', + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 50, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 101) + assert test(hass) + + hass.states.async_set("sensor.temperature", 50) + assert test(hass) + + hass.states.async_set("sensor.temperature", 49) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert not test(hass) + + async def test_time_window(hass): """Test time condition windows.""" sixam = dt.parse_time("06:00:00") @@ -194,9 +282,46 @@ async def test_extract_entities(): "entity_id": "sensor.temperature_2", "below": 110, }, + { + "condition": "not", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature_3", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature_4", + "below": 110, + }, + ], + }, + { + "condition": "or", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature_5", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature_6", + "below": 110, + }, + ], + }, ], } - ) == {"sensor.temperature", "sensor.temperature_2"} + ) == { + "sensor.temperature", + "sensor.temperature_2", + "sensor.temperature_3", + "sensor.temperature_4", + "sensor.temperature_5", + "sensor.temperature_6", + } async def test_extract_devices(): @@ -207,6 +332,41 @@ async def test_extract_devices(): "conditions": [ {"condition": "device", "device_id": "abcd", "domain": "light"}, {"condition": "device", "device_id": "qwer", "domain": "switch"}, + { + "condition": "state", + "entity_id": "sensor.not_a_device", + "state": "100", + }, + { + "condition": "not", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_not", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_not", + "domain": "switch", + }, + ], + }, + { + "condition": "or", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_or", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_or", + "domain": "switch", + }, + ], + }, ], } - ) == {"abcd", "qwer"} + ) == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"} diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 54003b9dd18..7130514f47f 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,15 +1,14 @@ """Tests for the Config Entry Flow helper.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.config import async_process_ha_core_config from homeassistant.helpers import config_entry_flow +from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, - mock_coro, mock_entity_platform, mock_integration, ) @@ -46,6 +45,7 @@ async def test_single_entry_allowed(hass, discovery_flow_conf): """Test only a single entry is allowed.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_user() @@ -69,6 +69,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): """Test user requires no confirmation to setup.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} discovery_flow_conf["discovered"] = True result = await flow.async_step_user() @@ -95,7 +96,7 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): """Test we ask for confirmation via discovery.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass - flow.context = {} + flow.context = {"source": source} result = await getattr(flow, f"async_step_{source}")({}) @@ -152,6 +153,7 @@ async def test_import_no_confirmation(hass, discovery_flow_conf): """Test import requires no confirmation to set up.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} discovery_flow_conf["discovered"] = True result = await flow.async_step_import(None) @@ -162,6 +164,7 @@ async def test_import_single_instance(hass, discovery_flow_conf): """Test import doesn't create second instance.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} discovery_flow_conf["discovered"] = True MockConfigEntry(domain="test").add_to_hass(hass) @@ -169,6 +172,38 @@ async def test_import_single_instance(hass, discovery_flow_conf): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT +async def test_ignored_discoveries(hass, discovery_flow_conf): + """Test we can ignore discovered entries.""" + mock_entity_platform(hass, "config_flow.test", None) + + result = await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + flow = next( + ( + flw + for flw in hass.config_entries.flow.async_progress() + if flw["flow_id"] == result["flow_id"] + ), + None, + ) + + # Ignore it. + await hass.config_entries.flow.async_init( + flow["handler"], + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": flow["context"]["unique_id"]}, + ) + + # Second discovery should be aborted + result = await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): """Test only a single entry is allowed.""" flow = config_entries.HANDLERS["test_single"]() @@ -198,7 +233,9 @@ async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): flow = config_entries.HANDLERS["test_single"]() flow.hass = hass - hass.config.api = Mock(base_url="http://example.com") + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) result = await flow.async_step_user(user_input={}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -209,8 +246,8 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): """Test only a single entry is allowed.""" assert await setup.async_setup_component(hass, "cloud", {}) - async_setup_entry = Mock(return_value=mock_coro(True)) - async_unload_entry = Mock(return_value=mock_coro(True)) + async_setup_entry = Mock(return_value=True) + async_unload_entry = Mock(return_value=True) mock_integration( hass, @@ -228,10 +265,9 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - coro = mock_coro({"cloudhook_url": "https://example.com"}) - with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value=coro + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://example.com"}, ) as mock_create, patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( @@ -246,7 +282,8 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): assert len(async_setup_entry.mock_calls) == 1 with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_delete", return_value=coro + "hass_nabucasa.cloudhooks.Cloudhooks.async_delete", + return_value={"cloudhook_url": "https://example.com"}, ) as mock_delete: result = await hass.config_entries.async_remove(result["result"].entry_id) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index a72f3f51ee7..801ea49bfbb 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -2,13 +2,14 @@ import asyncio import logging import time -from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.config import async_process_ha_core_config from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "oauth2_test" @@ -124,7 +125,9 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" - hass.config.api.base_url = "https://example.com" + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -140,7 +143,10 @@ async def test_step_discovery(hass, flow_handler, local_impl): async def test_abort_discovered_multiple(hass, flow_handler, local_impl): """Test if aborts when discovered multiple times.""" - hass.config.api.base_url = "https://example.com" + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) + flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -163,7 +169,9 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl): """Test if abort discovery when entries exists.""" - hass.config.api.base_url = "https://example.com" + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -184,7 +192,10 @@ async def test_full_flow( hass, flow_handler, local_impl, aiohttp_client, aioclient_mock ): """Check full flow.""" - hass.config.api.base_url = "https://example.com" + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) + flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1d082462849..72eb61bbacb 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -3,7 +3,6 @@ from datetime import date, datetime, timedelta import enum import os from socket import _GLOBAL_DEFAULT_TIMEOUT -from unittest.mock import Mock, patch import uuid import pytest @@ -12,6 +11,8 @@ import voluptuous as vol import homeassistant import homeassistant.helpers.config_validation as cv +from tests.async_mock import Mock, patch + def test_boolean(): """Test boolean validation.""" @@ -350,6 +351,26 @@ def test_string(): schema(value) +def test_string_with_no_html(): + """Test string with no html validation.""" + schema = vol.Schema(cv.string_with_no_html) + + with pytest.raises(vol.Invalid): + schema("This has HTML in it Link") + + with pytest.raises(vol.Invalid): + schema("Bold") + + for value in ( + True, + 3, + "Hello", + "**Hello**", + "This has no HTML [Link](https://home-assistant.io)", + ): + schema(value) + + def test_temperature_unit(): """Test temperature unit validation.""" schema = vol.Schema(cv.temperature_unit) diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index d51eb22c90d..d84b6fd7da7 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -1,8 +1,8 @@ """Tests for debounce.""" -from asynctest import CoroutineMock - from homeassistant.helpers import debounce +from tests.async_mock import AsyncMock + async def test_immediate_works(hass): """Test immediate works.""" @@ -12,7 +12,7 @@ async def test_immediate_works(hass): None, cooldown=0.01, immediate=True, - function=CoroutineMock(side_effect=lambda: calls.append(None)), + function=AsyncMock(side_effect=lambda: calls.append(None)), ) # Call when nothing happening @@ -57,7 +57,7 @@ async def test_not_immediate_works(hass): None, cooldown=0.01, immediate=False, - function=CoroutineMock(side_effect=lambda: calls.append(None)), + function=AsyncMock(side_effect=lambda: calls.append(None)), ) # Call when nothing happening diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 38410c3bf0f..c7e903f7b16 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,8 +1,8 @@ """Test deprecation helpers.""" -from unittest.mock import MagicMock, patch - from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated +from tests.async_mock import MagicMock, patch + class MockBaseClass: """Mock base class for deprecated testing.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index d5f762fc701..ef5f92de79c 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,14 +1,14 @@ """Tests for the Device Registry.""" import asyncio -from unittest.mock import patch -import asynctest import pytest -from homeassistant.core import callback -from homeassistant.helpers import device_registry +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, callback +from homeassistant.helpers import device_registry, entity_registry -from tests.common import flush_store, mock_device_registry +from tests.async_mock import patch +from tests.common import MockConfigEntry, flush_store, mock_device_registry @pytest.fixture @@ -149,6 +149,7 @@ async def test_loading_from_storage(hass, hass_storage): "model": "model", "name": "name", "sw_version": "version", + "entry_type": "service", "area_id": "12345A", "name_by_user": "Test Friendly Name", } @@ -168,6 +169,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.id == "abcdefghijklm" assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" + assert entry.entry_type == "service" assert isinstance(entry.config_entries, set) @@ -304,6 +306,9 @@ async def test_loading_saving_data(hass, registry): identifiers={("hue", "0123")}, manufacturer="manufacturer", model="via", + name="Original Name", + sw_version="Orig SW 1", + entry_type="device", ) orig_light = registry.async_get_or_create( @@ -317,6 +322,10 @@ async def test_loading_saving_data(hass, registry): assert len(registry.devices) == 2 + orig_via = registry.async_update_device( + orig_via.id, area_id="mock-area-id", name_by_user="mock-name-by-user" + ) + # Now load written data in new registry registry2 = device_registry.DeviceRegistry(hass) await flush_store(registry._store) @@ -474,7 +483,7 @@ async def test_update_remove_config_entries(hass, registry, update_events): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with asynctest.patch( + with patch( "homeassistant.helpers.device_registry.DeviceRegistry.async_load" ) as mock_load: results = await asyncio.gather( @@ -502,3 +511,76 @@ async def test_update_sw_version(registry): assert mock_save.call_count == 1 assert updated_entry != entry assert updated_entry.sw_version == sw_version + + +async def test_cleanup_device_registry(hass, registry): + """Test cleanup works.""" + config_entry = MockConfigEntry(domain="hue") + config_entry.add_to_hass(hass) + + d1 = registry.async_get_or_create( + identifiers={("hue", "d1")}, config_entry_id=config_entry.entry_id + ) + registry.async_get_or_create( + identifiers={("hue", "d2")}, config_entry_id=config_entry.entry_id + ) + d3 = registry.async_get_or_create( + identifiers={("hue", "d3")}, config_entry_id=config_entry.entry_id + ) + registry.async_get_or_create( + identifiers={("something", "d4")}, config_entry_id="non_existing" + ) + + ent_reg = await entity_registry.async_get_registry(hass) + ent_reg.async_get_or_create("light", "hue", "e1", device_id=d1.id) + ent_reg.async_get_or_create("light", "hue", "e2", device_id=d1.id) + ent_reg.async_get_or_create("light", "hue", "e3", device_id=d3.id) + + device_registry.async_cleanup(hass, registry, ent_reg) + + assert registry.async_get_device({("hue", "d1")}, set()) is not None + assert registry.async_get_device({("hue", "d2")}, set()) is None + assert registry.async_get_device({("hue", "d3")}, set()) is not None + assert registry.async_get_device({("something", "d4")}, set()) is None + + +async def test_cleanup_startup(hass): + """Test we run a cleanup on startup.""" + hass.state = CoreState.not_running + await device_registry.async_get_registry(hass) + + with patch( + "homeassistant.helpers.device_registry.Debouncer.async_call" + ) as mock_call: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_cleanup_entity_registry_change(hass): + """Test we run a cleanup when entity registry changes.""" + await device_registry.async_get_registry(hass) + ent_reg = await entity_registry.async_get_registry(hass) + + with patch( + "homeassistant.helpers.device_registry.Debouncer.async_call" + ) as mock_call: + entity = ent_reg.async_get_or_create("light", "hue", "e1") + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 0 + + # Normal update does not trigger + ent_reg.async_update_entity(entity.entity_id, name="updated") + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 0 + + # Device ID update triggers + ent_reg.async_get_or_create("light", "hue", "e1", device_id="bla") + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 1 + + # Removal also triggers + ent_reg.async_remove(entity.entity_id) + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 2 diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index d2aa939043c..09c7942d08f 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -1,6 +1,8 @@ """Test dispatcher helpers.""" from functools import partial +import pytest + from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -128,6 +130,7 @@ async def test_simple_function_multiargs(hass): assert calls == [3, 2, "bla"] +@pytest.mark.no_fail_on_log_exception async def test_callback_exception_gets_logged(hass, caplog): """Test exception raised by signal handler.""" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dd734bb0dcb..70b72b1752f 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import threading -from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -13,6 +12,7 @@ from homeassistant.core import Context from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.entity_values import EntityValues +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import get_test_home_assistant, mock_registry @@ -139,7 +139,7 @@ async def test_warn_slow_update(hass): mock_entity.entity_id = "comp_test.test_entity" mock_entity.async_update = async_update - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await mock_entity.async_update_ha_state(True) assert mock_call.called assert len(mock_call.mock_calls) == 2 @@ -169,7 +169,7 @@ async def test_warn_slow_update_with_exception(hass): mock_entity.entity_id = "comp_test.test_entity" mock_entity.async_update = async_update - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await mock_entity.async_update_ha_state(True) assert mock_call.called assert len(mock_call.mock_calls) == 2 @@ -198,7 +198,7 @@ async def test_warn_slow_device_update_disabled(hass): mock_entity.entity_id = "comp_test.test_entity" mock_entity.async_update = async_update - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await mock_entity.async_device_update(warning=False) assert not mock_call.called diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 1c5224d89c3..625f21d9d9f 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -3,9 +3,7 @@ from collections import OrderedDict from datetime import timedelta import logging -from unittest.mock import Mock, patch -import asynctest import pytest import voluptuous as vol @@ -17,13 +15,13 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockConfigEntry, MockEntity, MockModule, MockPlatform, async_fire_time_changed, - mock_coro, mock_entity_platform, mock_integration, ) @@ -82,13 +80,8 @@ async def test_setup_recovers_when_setup_raises(hass): assert platform2_setup.called -@asynctest.patch( - "homeassistant.helpers.entity_component.EntityComponent.async_setup_platform", - return_value=mock_coro(), -) -@asynctest.patch( - "homeassistant.setup.async_setup_component", return_value=mock_coro(True) -) +@patch("homeassistant.helpers.entity_component.EntityComponent.async_setup_platform",) +@patch("homeassistant.setup.async_setup_component", return_value=True) async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): """Test setup for discovery.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -105,7 +98,7 @@ async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): assert ("platform_test", {}, {"msg": "discovery_info"}) == mock_setup.call_args[0] -@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") +@patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_config(mock_track, hass): """Test the setting of the scan interval via configuration.""" @@ -295,7 +288,7 @@ async def test_setup_dependencies_platform(hass): async def test_setup_entry(hass): """Test setup entry calls async_setup_entry on platform.""" - mock_setup_entry = Mock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_entity_platform( hass, "test_domain.entry_domain", @@ -326,7 +319,7 @@ async def test_setup_entry_platform_not_exist(hass): async def test_setup_entry_fails_duplicate(hass): """Test we don't allow setting up a config entry twice.""" - mock_setup_entry = Mock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_entity_platform( hass, "test_domain.entry_domain", @@ -344,7 +337,7 @@ async def test_setup_entry_fails_duplicate(hass): async def test_unload_entry_resets_platform(hass): """Test unloading an entry removes all entities.""" - mock_setup_entry = Mock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_entity_platform( hass, "test_domain.entry_domain", @@ -380,7 +373,7 @@ async def test_update_entity(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) entity = MockEntity() entity.async_write_ha_state = Mock() - entity.async_update_ha_state = Mock(return_value=mock_coro()) + entity.async_update_ha_state = AsyncMock(return_value=None) await component.async_add_entities([entity]) # Called as part of async_add_entities diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index df247d82d5c..eb24ea971a7 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -2,9 +2,7 @@ import asyncio from datetime import timedelta import logging -from unittest.mock import MagicMock, Mock, patch -import asynctest import pytest from homeassistant.const import UNIT_PERCENTAGE @@ -18,6 +16,7 @@ from homeassistant.helpers.entity_component import ( ) import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockEntity, @@ -136,7 +135,7 @@ async def test_update_state_adds_entities_with_update_before_add_false(hass): assert not ent.update.called -@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") +@patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_platform(mock_track, hass): """Test the setting of the scan interval via platform.""" @@ -183,7 +182,7 @@ async def test_platform_warn_slow_setup(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await component.async_setup({DOMAIN: {"platform": "platform"}}) assert mock_call.called @@ -705,6 +704,7 @@ async def test_device_info_called(hass): "model": "test-model", "name": "test-name", "sw_version": "test-sw", + "entry_type": "service", "via_device": ("hue", "via-id"), }, ), @@ -731,6 +731,7 @@ async def test_device_info_called(hass): assert device.model == "test-model" assert device.name == "test-name" assert device.sw_version == "test-sw" + assert device.entry_type == "service" assert device.via_device_id == via.id diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cda2f1245fb..285f43b6d4d 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,14 +1,14 @@ """Tests for the Entity Registry.""" import asyncio -from unittest.mock import patch -import asynctest import pytest from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.helpers import entity_registry +import tests.async_mock +from tests.async_mock import patch from tests.common import MockConfigEntry, flush_store, mock_registry YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -241,12 +241,20 @@ async def test_loading_extra_values(hass, hass_storage): "unique_id": "disabled-hass", "disabled_by": "hass", }, + { + "entity_id": "test.invalid__entity", + "platform": "super_platform", + "unique_id": "invalid-hass", + "disabled_by": "hass", + }, ] }, } registry = await entity_registry.async_get_registry(hass) + assert len(registry.entities) == 4 + entry_with_name = registry.async_get_or_create( "test", "super_platform", "with-name" ) @@ -401,7 +409,7 @@ async def test_loading_invalid_entity_id(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.helpers.entity_registry.EntityRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index f6e375acf0b..654cf8483db 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,7 +1,6 @@ """Test event helpers.""" # pylint: disable=protected-access from datetime import datetime, timedelta -from unittest.mock import patch from astral import Astral import pytest @@ -27,6 +26,7 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py new file mode 100644 index 00000000000..2e8c83c6517 --- /dev/null +++ b/tests/helpers/test_frame.py @@ -0,0 +1,56 @@ +"""Test the frame helper.""" +import pytest + +from homeassistant.helpers import frame + +from tests.async_mock import Mock, patch + + +async def test_extract_frame_integration(caplog): + """Test extracting the current frame from integration context.""" + correct_frame = Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="self.light.is_on", + ) + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + found_frame, integration, path = frame.get_integration_frame() + + assert integration == "hue" + assert path == "homeassistant/components/" + assert found_frame == correct_frame + + +async def test_extract_frame_no_integration(caplog): + """Test extracting the current frame without integration context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ), pytest.raises(frame.MissingIntegrationFrame): + frame.get_integration_frame() diff --git a/tests/helpers/test_instance_id.py b/tests/helpers/test_instance_id.py new file mode 100644 index 00000000000..36e87b31a43 --- /dev/null +++ b/tests/helpers/test_instance_id.py @@ -0,0 +1,26 @@ +"""Tests for instance ID helper.""" +from tests.async_mock import patch + + +async def test_get_id_empty(hass, hass_storage): + """Get unique ID.""" + uuid = await hass.helpers.instance_id.async_get() + assert uuid is not None + # Assert it's stored + assert hass_storage["core.uuid"]["data"]["uuid"] == uuid + + +async def test_get_id_migrate(hass, hass_storage): + """Migrate existing file.""" + with patch( + "homeassistant.util.json.load_json", return_value={"uuid": "1234"} + ), patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove: + uuid = await hass.helpers.instance_id.async_get() + + assert uuid == "1234" + + # Assert it's stored + assert hass_storage["core.uuid"]["data"]["uuid"] == uuid + + # assert old deleted + assert len(mock_remove.mock_calls) == 1 diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index d6c844c0d91..6f0e56d34c8 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -1,8 +1,7 @@ """Test integration platform helpers.""" -from unittest.mock import Mock - from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED +from tests.async_mock import Mock from tests.common import mock_platform diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index d4c5366b879..f6665b054e7 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,34 +1,628 @@ """Test network helper.""" -from unittest.mock import Mock, patch +import pytest from homeassistant.components import cloud -from homeassistant.helpers import network +from homeassistant.config import async_process_ha_core_config +from homeassistant.core import HomeAssistant +from homeassistant.helpers.network import ( + NoURLAvailableError, + _get_cloud_url, + _get_deprecated_base_url, + _get_external_url, + _get_internal_url, + get_url, +) + +from tests.async_mock import Mock, patch -async def test_get_external_url(hass): - """Test get_external_url.""" - hass.config.api = Mock(base_url="http://192.168.1.100:8123") +async def test_get_url_internal(hass: HomeAssistant): + """Test getting an instance URL when the user has set an internal URL.""" + assert hass.config.internal_url is None - assert network.async_get_external_url(hass) is None + # Test with internal URL: http://example.local:8123 + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) - hass.config.api = Mock(base_url="http://example.duckdns.org:8123") + assert hass.config.internal_url == "http://example.local:8123" + assert _get_internal_url(hass) == "http://example.local:8123" + assert _get_internal_url(hass, allow_ip=False) == "http://example.local:8123" - assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + # Test with internal URL: https://example.local:8123 + await async_process_ha_core_config( + hass, {"internal_url": "https://example.local:8123"}, + ) + + assert hass.config.internal_url == "https://example.local:8123" + assert _get_internal_url(hass) == "https://example.local:8123" + assert _get_internal_url(hass, allow_ip=False) == "https://example.local:8123" + assert _get_internal_url(hass, require_ssl=True) == "https://example.local:8123" + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + + # Test with internal URL: http://example.local:80/ + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:80/"}, + ) + + assert hass.config.internal_url == "http://example.local:80/" + assert _get_internal_url(hass) == "http://example.local" + assert _get_internal_url(hass, allow_ip=False) == "http://example.local" + assert _get_internal_url(hass, require_standard_port=True) == "http://example.local" + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + # Test with internal URL: https://example.local:443 + await async_process_ha_core_config( + hass, {"internal_url": "https://example.local:443"}, + ) + + assert hass.config.internal_url == "https://example.local:443" + assert _get_internal_url(hass) == "https://example.local" + assert _get_internal_url(hass, allow_ip=False) == "https://example.local" + assert ( + _get_internal_url(hass, require_standard_port=True) == "https://example.local" + ) + assert _get_internal_url(hass, require_ssl=True) == "https://example.local" + + # Test with internal URL: https://192.168.0.1 + await async_process_ha_core_config( + hass, {"internal_url": "https://192.168.0.1"}, + ) + + assert hass.config.internal_url == "https://192.168.0.1" + assert _get_internal_url(hass) == "https://192.168.0.1" + assert _get_internal_url(hass, require_standard_port=True) == "https://192.168.0.1" + assert _get_internal_url(hass, require_ssl=True) == "https://192.168.0.1" + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + # Test with internal URL: http://192.168.0.1:8123 + await async_process_ha_core_config( + hass, {"internal_url": "http://192.168.0.1:8123"}, + ) + + assert hass.config.internal_url == "http://192.168.0.1:8123" + assert _get_internal_url(hass) == "http://192.168.0.1:8123" + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + +async def test_get_url_internal_fallback(hass: HomeAssistant): + """Test getting an instance URL when the user has not set an internal URL.""" + assert hass.config.internal_url is None + + hass.config.api = Mock( + use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123" + ) + assert _get_internal_url(hass) == "http://192.168.123.123:8123" + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + hass.config.api = Mock( + use_ssl=False, port=80, deprecated_base_url=None, local_ip="192.168.123.123" + ) + assert _get_internal_url(hass) == "http://192.168.123.123" + assert ( + _get_internal_url(hass, require_standard_port=True) == "http://192.168.123.123" + ) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + hass.config.api = Mock(use_ssl=True, port=443, deprecated_base_url=None) + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + # Do no accept any local loopback address as fallback + hass.config.api = Mock( + use_ssl=False, port=80, deprecated_base_url=None, local_ip="127.0.0.1" + ) + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + +async def test_get_url_external(hass: HomeAssistant): + """Test getting an instance URL when the user has set an external URL.""" + assert hass.config.external_url is None + + # Test with external URL: http://example.com:8123 + await async_process_ha_core_config( + hass, {"external_url": "http://example.com:8123"}, + ) + + assert hass.config.external_url == "http://example.com:8123" + assert _get_external_url(hass) == "http://example.com:8123" + assert _get_external_url(hass, allow_cloud=False) == "http://example.com:8123" + assert _get_external_url(hass, allow_ip=False) == "http://example.com:8123" + assert _get_external_url(hass, prefer_cloud=True) == "http://example.com:8123" + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_ssl=True) + + # Test with external URL: http://example.com:80/ + await async_process_ha_core_config( + hass, {"external_url": "http://example.com:80/"}, + ) + + assert hass.config.external_url == "http://example.com:80/" + assert _get_external_url(hass) == "http://example.com" + assert _get_external_url(hass, allow_cloud=False) == "http://example.com" + assert _get_external_url(hass, allow_ip=False) == "http://example.com" + assert _get_external_url(hass, prefer_cloud=True) == "http://example.com" + assert _get_external_url(hass, require_standard_port=True) == "http://example.com" + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_ssl=True) + + # Test with external url: https://example.com:443/ + await async_process_ha_core_config( + hass, {"external_url": "https://example.com:443/"}, + ) + assert hass.config.external_url == "https://example.com:443/" + assert _get_external_url(hass) == "https://example.com" + assert _get_external_url(hass, allow_cloud=False) == "https://example.com" + assert _get_external_url(hass, allow_ip=False) == "https://example.com" + assert _get_external_url(hass, prefer_cloud=True) == "https://example.com" + assert _get_external_url(hass, require_ssl=False) == "https://example.com" + assert _get_external_url(hass, require_standard_port=True) == "https://example.com" + + # Test with external URL: https://example.com:80 + await async_process_ha_core_config( + hass, {"external_url": "https://example.com:80"}, + ) + assert hass.config.external_url == "https://example.com:80" + assert _get_external_url(hass) == "https://example.com:80" + assert _get_external_url(hass, allow_cloud=False) == "https://example.com:80" + assert _get_external_url(hass, allow_ip=False) == "https://example.com:80" + assert _get_external_url(hass, prefer_cloud=True) == "https://example.com:80" + assert _get_external_url(hass, require_ssl=True) == "https://example.com:80" + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_standard_port=True) + + # Test with external URL: https://192.168.0.1 + await async_process_ha_core_config( + hass, {"external_url": "https://192.168.0.1"}, + ) + assert hass.config.external_url == "https://192.168.0.1" + assert _get_external_url(hass) == "https://192.168.0.1" + assert _get_external_url(hass, allow_cloud=False) == "https://192.168.0.1" + assert _get_external_url(hass, prefer_cloud=True) == "https://192.168.0.1" + assert _get_external_url(hass, require_standard_port=True) == "https://192.168.0.1" + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_ssl=True) + + +async def test_get_cloud_url(hass: HomeAssistant): + """Test getting an instance URL when the user has set an external URL.""" + assert hass.config.external_url is None hass.config.components.add("cloud") - assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" - - with patch.object( - hass.components.cloud, - "async_remote_ui_url", - side_effect=cloud.CloudNotAvailable, - ): - assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" - with patch.object( hass.components.cloud, "async_remote_ui_url", return_value="https://example.nabu.casa", ): - assert network.async_get_external_url(hass) == "https://example.nabu.casa" + assert _get_cloud_url(hass) == "https://example.nabu.casa" + + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + side_effect=cloud.CloudNotAvailable, + ): + with pytest.raises(NoURLAvailableError): + _get_cloud_url(hass) + + +async def test_get_external_url_cloud_fallback(hass: HomeAssistant): + """Test getting an external instance URL with cloud fallback.""" + assert hass.config.external_url is None + + # Test with external URL: http://1.1.1.1:8123 + await async_process_ha_core_config( + hass, {"external_url": "http://1.1.1.1:8123"}, + ) + + assert hass.config.external_url == "http://1.1.1.1:8123" + assert _get_external_url(hass, prefer_cloud=True) == "http://1.1.1.1:8123" + + # Add Cloud to the previous test + hass.config.components.add("cloud") + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + return_value="https://example.nabu.casa", + ): + assert _get_external_url(hass, allow_cloud=False) == "http://1.1.1.1:8123" + assert _get_external_url(hass, allow_ip=False) == "https://example.nabu.casa" + assert _get_external_url(hass, prefer_cloud=False) == "http://1.1.1.1:8123" + assert _get_external_url(hass, prefer_cloud=True) == "https://example.nabu.casa" + assert _get_external_url(hass, require_ssl=True) == "https://example.nabu.casa" + assert ( + _get_external_url(hass, require_standard_port=True) + == "https://example.nabu.casa" + ) + + # Test with external URL: https://example.com + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) + + assert hass.config.external_url == "https://example.com" + assert _get_external_url(hass, prefer_cloud=True) == "https://example.com" + + # Add Cloud to the previous test + hass.config.components.add("cloud") + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + return_value="https://example.nabu.casa", + ): + assert _get_external_url(hass, allow_cloud=False) == "https://example.com" + assert _get_external_url(hass, allow_ip=False) == "https://example.com" + assert _get_external_url(hass, prefer_cloud=False) == "https://example.com" + assert _get_external_url(hass, prefer_cloud=True) == "https://example.nabu.casa" + assert _get_external_url(hass, require_ssl=True) == "https://example.com" + assert ( + _get_external_url(hass, require_standard_port=True) == "https://example.com" + ) + assert ( + _get_external_url(hass, prefer_cloud=True, allow_cloud=False) + == "https://example.com" + ) + + +async def test_get_url(hass: HomeAssistant): + """Test getting an instance URL.""" + assert hass.config.external_url is None + assert hass.config.internal_url is None + + with pytest.raises(NoURLAvailableError): + get_url(hass) + + hass.config.api = Mock( + use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123" + ) + assert get_url(hass) == "http://192.168.123.123:8123" + assert get_url(hass, prefer_external=True) == "http://192.168.123.123:8123" + + with pytest.raises(NoURLAvailableError): + get_url(hass, allow_internal=False) + + # Test only external + hass.config.api = None + await async_process_ha_core_config( + hass, {"external_url": "https://example.com"}, + ) + assert hass.config.external_url == "https://example.com" + assert hass.config.internal_url is None + assert get_url(hass) == "https://example.com" + + # Test preference or allowance + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local", "external_url": "https://example.com"}, + ) + assert hass.config.external_url == "https://example.com" + assert hass.config.internal_url == "http://example.local" + assert get_url(hass) == "http://example.local" + assert get_url(hass, prefer_external=True) == "https://example.com" + assert get_url(hass, allow_internal=False) == "https://example.com" + assert ( + get_url(hass, prefer_external=True, allow_external=False) + == "http://example.local" + ) + + with pytest.raises(NoURLAvailableError): + get_url(hass, allow_external=False, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + get_url(hass, allow_external=False, allow_internal=False) + + +async def test_get_deprecated_base_url_internal(hass: HomeAssistant): + """Test getting an internal instance URL from the deprecated base_url.""" + # Test with SSL local URL + hass.config.api = Mock(deprecated_base_url="https://example.local") + assert _get_deprecated_base_url(hass, internal=True) == "https://example.local" + assert ( + _get_deprecated_base_url(hass, internal=True, allow_ip=False) + == "https://example.local" + ) + assert ( + _get_deprecated_base_url(hass, internal=True, require_ssl=True) + == "https://example.local" + ) + assert ( + _get_deprecated_base_url(hass, internal=True, require_standard_port=True) + == "https://example.local" + ) + + # Test with no SSL, local IP URL + hass.config.api = Mock(deprecated_base_url="http://10.10.10.10:8123") + assert _get_deprecated_base_url(hass, internal=True) == "http://10.10.10.10:8123" + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, require_standard_port=True) + + # Test with SSL, local IP URL + hass.config.api = Mock(deprecated_base_url="https://10.10.10.10") + assert _get_deprecated_base_url(hass, internal=True) == "https://10.10.10.10" + assert ( + _get_deprecated_base_url(hass, internal=True, require_ssl=True) + == "https://10.10.10.10" + ) + assert ( + _get_deprecated_base_url(hass, internal=True, require_standard_port=True) + == "https://10.10.10.10" + ) + + # Test external URL + hass.config.api = Mock(deprecated_base_url="https://example.com") + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, allow_ip=False) + + # Test with loopback + hass.config.api = Mock(deprecated_base_url="https://127.0.0.42") + with pytest.raises(NoURLAvailableError): + assert _get_deprecated_base_url(hass, internal=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, internal=True, require_standard_port=True) + + +async def test_get_deprecated_base_url_external(hass: HomeAssistant): + """Test getting an external instance URL from the deprecated base_url.""" + # Test with SSL and external domain on standard port + hass.config.api = Mock(deprecated_base_url="https://example.com:443/") + assert _get_deprecated_base_url(hass) == "https://example.com" + assert _get_deprecated_base_url(hass, require_ssl=True) == "https://example.com" + assert ( + _get_deprecated_base_url(hass, require_standard_port=True) + == "https://example.com" + ) + + # Test without SSL and external domain on non-standard port + hass.config.api = Mock(deprecated_base_url="http://example.com:8123/") + assert _get_deprecated_base_url(hass) == "http://example.com:8123" + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_standard_port=True) + + # Test SSL on external IP + hass.config.api = Mock(deprecated_base_url="https://1.1.1.1") + assert _get_deprecated_base_url(hass) == "https://1.1.1.1" + assert _get_deprecated_base_url(hass, require_ssl=True) == "https://1.1.1.1" + assert ( + _get_deprecated_base_url(hass, require_standard_port=True) == "https://1.1.1.1" + ) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, allow_ip=False) + + # Test with private IP + hass.config.api = Mock(deprecated_base_url="https://10.10.10.10") + with pytest.raises(NoURLAvailableError): + assert _get_deprecated_base_url(hass) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_standard_port=True) + + # Test with local domain + hass.config.api = Mock(deprecated_base_url="https://example.local") + with pytest.raises(NoURLAvailableError): + assert _get_deprecated_base_url(hass) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_standard_port=True) + + # Test with loopback + hass.config.api = Mock(deprecated_base_url="https://127.0.0.42") + with pytest.raises(NoURLAvailableError): + assert _get_deprecated_base_url(hass) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_ssl=True) + + with pytest.raises(NoURLAvailableError): + _get_deprecated_base_url(hass, require_standard_port=True) + + +async def test_get_internal_url_with_base_url_fallback(hass: HomeAssistant): + """Test getting an internal instance URL with the deprecated base_url fallback.""" + hass.config.api = Mock( + use_ssl=False, port=8123, deprecated_base_url=None, local_ip="192.168.123.123" + ) + assert hass.config.internal_url is None + assert _get_internal_url(hass) == "http://192.168.123.123:8123" + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_standard_port=True) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_ssl=True) + + # Add base_url + hass.config.api = Mock( + use_ssl=False, port=8123, deprecated_base_url="https://example.local" + ) + assert _get_internal_url(hass) == "https://example.local" + assert _get_internal_url(hass, allow_ip=False) == "https://example.local" + assert ( + _get_internal_url(hass, require_standard_port=True) == "https://example.local" + ) + assert _get_internal_url(hass, require_ssl=True) == "https://example.local" + + # Add internal URL + await async_process_ha_core_config( + hass, {"internal_url": "https://internal.local"}, + ) + assert _get_internal_url(hass) == "https://internal.local" + assert _get_internal_url(hass, allow_ip=False) == "https://internal.local" + assert ( + _get_internal_url(hass, require_standard_port=True) == "https://internal.local" + ) + assert _get_internal_url(hass, require_ssl=True) == "https://internal.local" + + # Add internal URL, mixed results + await async_process_ha_core_config( + hass, {"internal_url": "http://internal.local:8123"}, + ) + assert _get_internal_url(hass) == "http://internal.local:8123" + assert _get_internal_url(hass, allow_ip=False) == "http://internal.local:8123" + assert ( + _get_internal_url(hass, require_standard_port=True) == "https://example.local" + ) + assert _get_internal_url(hass, require_ssl=True) == "https://example.local" + + # Add internal URL set to an IP + await async_process_ha_core_config( + hass, {"internal_url": "http://10.10.10.10:8123"}, + ) + assert _get_internal_url(hass) == "http://10.10.10.10:8123" + assert _get_internal_url(hass, allow_ip=False) == "https://example.local" + assert ( + _get_internal_url(hass, require_standard_port=True) == "https://example.local" + ) + assert _get_internal_url(hass, require_ssl=True) == "https://example.local" + + +async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): + """Test getting an external instance URL with the deprecated base_url fallback.""" + hass.config.api = Mock(use_ssl=False, port=8123, deprecated_base_url=None) + assert hass.config.internal_url is None + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass) + + # Test with SSL and external domain on standard port + hass.config.api = Mock(deprecated_base_url="https://example.com:443/") + assert _get_external_url(hass) == "https://example.com" + assert _get_external_url(hass, allow_ip=False) == "https://example.com" + assert _get_external_url(hass, require_ssl=True) == "https://example.com" + assert _get_external_url(hass, require_standard_port=True) == "https://example.com" + + # Add external URL + await async_process_ha_core_config( + hass, {"external_url": "https://external.example.com"}, + ) + assert _get_external_url(hass) == "https://external.example.com" + assert _get_external_url(hass, allow_ip=False) == "https://external.example.com" + assert ( + _get_external_url(hass, require_standard_port=True) + == "https://external.example.com" + ) + assert _get_external_url(hass, require_ssl=True) == "https://external.example.com" + + # Add external URL, mixed results + await async_process_ha_core_config( + hass, {"external_url": "http://external.example.com:8123"}, + ) + assert _get_external_url(hass) == "http://external.example.com:8123" + assert _get_external_url(hass, allow_ip=False) == "http://external.example.com:8123" + assert _get_external_url(hass, require_standard_port=True) == "https://example.com" + assert _get_external_url(hass, require_ssl=True) == "https://example.com" + + # Add external URL set to an IP + await async_process_ha_core_config( + hass, {"external_url": "http://1.1.1.1:8123"}, + ) + assert _get_external_url(hass) == "http://1.1.1.1:8123" + assert _get_external_url(hass, allow_ip=False) == "https://example.com" + assert _get_external_url(hass, require_standard_port=True) == "https://example.com" + assert _get_external_url(hass, require_ssl=True) == "https://example.com" diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index da2acee6625..7866662266d 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,8 +1,6 @@ """The tests for the Restore component.""" from datetime import datetime -from asynctest import patch - from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, State from homeassistant.exceptions import HomeAssistantError @@ -16,7 +14,7 @@ from homeassistant.helpers.restore_state import ( ) from homeassistant.util import dt as dt_util -from tests.common import mock_coro +from tests.async_mock import patch async def test_caching_data(hass): @@ -193,7 +191,7 @@ async def test_dump_error(hass): with patch( "homeassistant.helpers.restore_state.Store.async_save", - return_value=mock_coro(exception=HomeAssistantError), + side_effect=HomeAssistantError, ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): await data.async_dump_states() @@ -208,7 +206,7 @@ async def test_load_error(hass): with patch( "homeassistant.helpers.storage.Store.async_load", - return_value=mock_coro(exception=HomeAssistantError), + side_effect=HomeAssistantError, ): state = await entity.async_get_last_state() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 9d7e7751c10..c00dadc27e8 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,7 +6,6 @@ from datetime import timedelta import logging from unittest import mock -import asynctest import pytest import voluptuous as vol @@ -19,6 +18,7 @@ from homeassistant.helpers import config_validation as cv, script from homeassistant.helpers.event import async_call_later import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( async_capture_events, async_fire_time_changed, @@ -776,7 +776,7 @@ async def test_condition_basic(hass, script_mode): @pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) -@asynctest.patch("homeassistant.helpers.script.condition.async_from_config") +@patch("homeassistant.helpers.script.condition.async_from_config") async def test_condition_created_once(async_from_config, hass, script_mode): """Test that the conditions do not get created multiple times.""" sequence = cv.SCRIPT_SCHEMA( diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 04d0ec64b83..e87fd2646dd 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -2,7 +2,6 @@ from collections import OrderedDict from copy import deepcopy import unittest -from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -27,10 +26,10 @@ from homeassistant.helpers import ( import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockEntity, get_test_home_assistant, - mock_coro, mock_device_registry, mock_registry, mock_service, @@ -41,8 +40,7 @@ from tests.common import ( def mock_handle_entity_call(): """Mock service platform call.""" with patch( - "homeassistant.helpers.service._handle_entity_call", - side_effect=lambda *args: mock_coro(), + "homeassistant.helpers.service._handle_entity_call", return_value=None, ) as mock_call: yield mock_call @@ -310,7 +308,7 @@ async def test_async_get_all_descriptions(hass): async def test_call_with_required_features(hass, mock_entities): """Test service calls invoked only if entity has required feautres.""" - test_service_mock = Mock(return_value=mock_coro()) + test_service_mock = AsyncMock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -325,7 +323,7 @@ async def test_call_with_required_features(hass, mock_entities): async def test_call_with_sync_func(hass, mock_entities): """Test invoking sync service calls.""" - test_service_mock = Mock() + test_service_mock = Mock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -337,7 +335,7 @@ async def test_call_with_sync_func(hass, mock_entities): async def test_call_with_sync_attr(hass, mock_entities): """Test invoking sync service calls.""" - mock_method = mock_entities["light.kitchen"].sync_method = Mock() + mock_method = mock_entities["light.kitchen"].sync_method = Mock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -374,11 +372,9 @@ async def test_call_context_target_all(hass, mock_handle_entity_call, mock_entit """Check we only target allowed entities if targeting all.""" with patch( "homeassistant.auth.AuthManager.async_get_user", - return_value=mock_coro( - Mock( - permissions=PolicyPermissions( - {"entities": {"entity_ids": {"light.kitchen": True}}}, None - ) + return_value=Mock( + permissions=PolicyPermissions( + {"entities": {"entity_ids": {"light.kitchen": True}}}, None ) ), ): @@ -404,11 +400,9 @@ async def test_call_context_target_specific( """Check targeting specific entities.""" with patch( "homeassistant.auth.AuthManager.async_get_user", - return_value=mock_coro( - Mock( - permissions=PolicyPermissions( - {"entities": {"entity_ids": {"light.kitchen": True}}}, None - ) + return_value=Mock( + permissions=PolicyPermissions( + {"entities": {"entity_ids": {"light.kitchen": True}}}, None ) ), ): @@ -435,7 +429,7 @@ async def test_call_context_target_specific_no_auth( with pytest.raises(exceptions.Unauthorized) as err: with patch( "homeassistant.auth.AuthManager.async_get_user", - return_value=mock_coro(Mock(permissions=PolicyPermissions({}, None))), + return_value=Mock(permissions=PolicyPermissions({}, None)), ): await service.entity_service_call( hass, @@ -606,7 +600,7 @@ async def test_domain_control_unknown(hass, mock_entities): with patch( "homeassistant.helpers.entity_registry.async_get_registry", - return_value=mock_coro(Mock(entities=mock_entities)), + return_value=Mock(entities=mock_entities), ): protected_mock_service = hass.helpers.service.verify_domain_control( "test_domain" diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index d8202b88b46..b19669d13f4 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,7 +1,6 @@ """Test state helpers.""" import asyncio from datetime import timedelta -from unittest.mock import patch import pytest @@ -22,6 +21,7 @@ import homeassistant.core as ha from homeassistant.helpers import state from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 61648c85ada..6325294033f 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,7 +2,6 @@ import asyncio from datetime import timedelta import json -from unittest.mock import Mock, patch import pytest @@ -14,7 +13,8 @@ from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import Mock, patch +from tests.common import async_fire_time_changed MOCK_VERSION = 1 MOCK_KEY = "storage-test" @@ -189,7 +189,7 @@ async def test_writing_while_writing_delay(hass, store, hass_storage): async def test_migrator_no_existing_config(hass, store, hass_storage): """Test migrator with no existing config.""" with patch("os.path.isfile", return_value=False), patch.object( - store, "async_load", return_value=mock_coro({"cur": "config"}) + store, "async_load", return_value={"cur": "config"} ): data = await storage.async_migrator(hass, "old-path", store) diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index b8ecd1ed86a..a877c7cdb00 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,12 +1,13 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access from datetime import datetime, timedelta -from unittest.mock import patch from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET import homeassistant.helpers.sun as sun import homeassistant.util.dt as dt_util +from tests.async_mock import patch + def test_next_events(hass): """Test retrieving next sun events.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6b3e0774bd8..3f3eb7a800c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2,7 +2,6 @@ from datetime import datetime import math import random -from unittest.mock import patch import pytest import pytz @@ -21,6 +20,8 @@ from homeassistant.helpers import template import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem +from tests.async_mock import patch + def _set_up_units(hass): """Set up the tests.""" @@ -1551,19 +1552,19 @@ def test_closest_function_no_location_states(hass): def test_extract_entities_none_exclude_stuff(hass): """Test extract entities function with none or exclude stuff.""" - assert template.extract_entities(None) == [] + assert template.extract_entities(hass, None) == [] - assert template.extract_entities("mdi:water") == [] + assert template.extract_entities(hass, "mdi:water") == [] assert ( template.extract_entities( - "{{ closest(states.zone.far_away, states.test_domain).entity_id }}" + hass, "{{ closest(states.zone.far_away, states.test_domain).entity_id }}" ) == MATCH_ALL ) assert ( - template.extract_entities('{{ distance("123", states.test_object_2) }}') + template.extract_entities(hass, '{{ distance("123", states.test_object_2) }}') == MATCH_ALL ) @@ -1571,7 +1572,9 @@ def test_extract_entities_none_exclude_stuff(hass): def test_extract_entities_no_match_entities(hass): """Test extract entities function with none entities stuff.""" assert ( - template.extract_entities("{{ value_json.tst | timestamp_custom('%Y' True) }}") + template.extract_entities( + hass, "{{ value_json.tst | timestamp_custom('%Y' True) }}" + ) == MATCH_ALL ) @@ -1670,35 +1673,38 @@ def test_generate_select(hass): ) -def test_extract_entities_match_entities(hass): +async def test_extract_entities_match_entities(hass): """Test extract entities function with entities stuff.""" assert ( template.extract_entities( + hass, """ {% if is_state('device_tracker.phone_1', 'home') %} Ha, Hercules is home! {% else %} Hercules is at {{ states('device_tracker.phone_1') }}. {% endif %} - """ + """, ) == ["device_tracker.phone_1"] ) assert ( template.extract_entities( + hass, """ {{ as_timestamp(states.binary_sensor.garage_door.last_changed) }} - """ + """, ) == ["binary_sensor.garage_door"] ) assert ( template.extract_entities( + hass, """ {{ states("binary_sensor.garage_door") }} - """ + """, ) == ["binary_sensor.garage_door"] ) @@ -1707,33 +1713,36 @@ Hercules is at {{ states('device_tracker.phone_1') }}. assert ( template.extract_entities( + hass, """ {{ is_state_attr('device_tracker.phone_2', 'battery', 40) }} - """ + """, ) == ["device_tracker.phone_2"] ) assert sorted(["device_tracker.phone_1", "device_tracker.phone_2"]) == sorted( template.extract_entities( + hass, """ {% if is_state('device_tracker.phone_1', 'home') %} Ha, Hercules is home! {% elif states.device_tracker.phone_2.attributes.battery < 40 %} Hercules you power goes done!. {% endif %} - """ + """, ) ) assert sorted(["sensor.pick_humidity", "sensor.pick_temperature"]) == sorted( template.extract_entities( + hass, """ {{ states.sensor.pick_temperature.state ~ „°C (“ ~ states.sensor.pick_humidity.state ~ „ %“ }} - """ + """, ) ) @@ -1741,35 +1750,55 @@ states.sensor.pick_humidity.state ~ „ %“ ["sensor.luftfeuchtigkeit_mean", "input_number.luftfeuchtigkeit"] ) == sorted( template.extract_entities( + hass, "{% if (states('sensor.luftfeuchtigkeit_mean') | int)" " > (states('input_number.luftfeuchtigkeit') | int +1.5)" - " %}true{% endif %}" + " %}true{% endif %}", ) ) + await group.Group.async_create_group(hass, "empty group", []) + + assert ["group.empty_group"] == template.extract_entities( + hass, "{{ expand('group.empty_group') | list | length }}" + ) + + hass.states.async_set("test_domain.object", "exists") + await group.Group.async_create_group(hass, "expand group", ["test_domain.object"]) + + assert sorted(["group.expand_group", "test_domain.object"]) == sorted( + template.extract_entities( + hass, "{{ expand('group.expand_group') | list | length }}" + ) + ) + + assert ["test_domain.entity"] == template.Template( + '{{ is_state("test_domain.entity", "on") }}', hass + ).extract_entities() + def test_extract_entities_with_variables(hass): """Test extract entities function with variables and entities stuff.""" hass.states.async_set("input_boolean.switch", "on") - assert {"input_boolean.switch"} == extract_entities( + assert ["input_boolean.switch"] == template.extract_entities( hass, "{{ is_state('input_boolean.switch', 'off') }}", {} ) - assert {"input_boolean.switch"} == extract_entities( + assert ["input_boolean.switch"] == template.extract_entities( hass, "{{ is_state(trigger.entity_id, 'off') }}", {"trigger": {"entity_id": "input_boolean.switch"}}, ) - assert {"no_state"} == extract_entities( + assert MATCH_ALL == template.extract_entities( hass, "{{ is_state(data, 'off') }}", {"data": "no_state"} ) - assert {"input_boolean.switch"} == extract_entities( + assert ["input_boolean.switch"] == template.extract_entities( hass, "{{ is_state(data, 'off') }}", {"data": "input_boolean.switch"} ) - assert {"input_boolean.switch"} == extract_entities( + assert ["input_boolean.switch"] == template.extract_entities( hass, "{{ is_state(trigger.entity_id, 'off') }}", {"trigger": {"entity_id": "input_boolean.switch"}}, diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 94e3829afee..77e55a1d6ed 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -3,14 +3,15 @@ import asyncio from os import path import pathlib -from asynctest import Mock, patch import pytest from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.generated import config_flows from homeassistant.helpers import translation from homeassistant.loader import async_get_integration -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component + +from tests.async_mock import Mock, patch @pytest.fixture @@ -142,11 +143,11 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): integration = Mock(file_path=pathlib.Path(__file__)) integration.name = "Component 1" - with patch.object( - translation, "component_translation_path", return_value="bla.json" - ), patch.object( - translation, - "load_translations_files", + with patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), patch( + "homeassistant.helpers.translation.load_translations_files", return_value={"component1": {"hello": "world"}}, ), patch( "homeassistant.helpers.translation.async_get_integration", @@ -169,16 +170,18 @@ async def test_get_translations_while_loading_components(hass): integration.name = "Component 1" hass.config.components.add("component1") - async def mock_load_translation_files(files): + def mock_load_translation_files(files): """Mock load translation files.""" # Mimic race condition by loading a component during setup - await async_setup_component(hass, "persistent_notification", {}) + setup_component(hass, "persistent_notification", {}) return {"component1": {"hello": "world"}} - with patch.object( - translation, "component_translation_path", return_value="bla.json" - ), patch.object( - translation, "load_translations_files", side_effect=mock_load_translation_files, + with patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), patch( + "homeassistant.helpers.translation.load_translations_files", + mock_load_translation_files, ), patch( "homeassistant.helpers.translation.async_get_integration", return_value=integration, @@ -229,9 +232,8 @@ async def test_translation_merging(hass, caplog): result["sensor.season"] = {"state": "bad data"} return result - with patch.object( - translation, - "load_translations_files", + with patch( + "homeassistant.helpers.translation.load_translations_files", side_effect=mock_load_translations_files, ): translations = await translation.async_get_translations(hass, "en", "state") diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 897367619ed..8d4f6934d78 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -4,12 +4,12 @@ from datetime import timedelta import logging import aiohttp -from asynctest import CoroutineMock, Mock import pytest from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, Mock from tests.common import async_fire_time_changed LOGGER = logging.getLogger(__name__) @@ -89,7 +89,7 @@ async def test_request_refresh(crd): ) async def test_refresh_known_errors(err_msg, crd, caplog): """Test raising known errors.""" - crd.update_method = CoroutineMock(side_effect=err_msg[0]) + crd.update_method = AsyncMock(side_effect=err_msg[0]) await crd.async_refresh() @@ -102,7 +102,7 @@ async def test_refresh_fail_unknown(crd, caplog): """Test raising unknown error.""" await crd.async_refresh() - crd.update_method = CoroutineMock(side_effect=ValueError) + crd.update_method = AsyncMock(side_effect=ValueError) await crd.async_refresh() diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py new file mode 100644 index 00000000000..20d32202a1a --- /dev/null +++ b/tests/ignore_uncaught_exceptions.py @@ -0,0 +1,16 @@ +"""List of tests that have uncaught exceptions today. Will be shrunk over time.""" +IGNORE_UNCAUGHT_EXCEPTIONS = [ + ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup",), + ( + "tests.components.owntracks.test_device_tracker", + "test_mobile_multiple_async_enter_exit", + ), + ( + "tests.components.smartthings.test_init", + "test_event_handler_dispatches_updated_devices", + ), + ( + "tests.components.unifi.test_controller", + "test_wireless_client_event_calls_update_wireless_devices", + ), +] diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 9089f159761..1049108f9de 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -1,8 +1,8 @@ """Mock helpers for Z-Wave component.""" -from unittest.mock import MagicMock - from pydispatch import dispatcher +from tests.async_mock import MagicMock + def value_changed(value): """Fire a value changed.""" diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 3ab19450879..c9ada99dc29 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,11 +1,10 @@ """Test the auth script to manage local users.""" -from unittest.mock import Mock, patch - import pytest from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.scripts import auth as script_auth +from tests.async_mock import Mock, patch from tests.common import register_auth_provider diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 737c3b56ecf..d28b5f69530 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,10 +1,10 @@ """Test check_config script.""" import logging -from unittest.mock import patch from homeassistant.config import YAML_CONFIG_FILE import homeassistant.scripts.check_config as check_config +from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 8feef2d3384..2c14bfdcf0a 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -1,8 +1,8 @@ """Test script init.""" -from unittest.mock import patch - import homeassistant.scripts as scripts +from tests.async_mock import patch + @patch("homeassistant.scripts.get_default_config_dir", return_value="/default") def test_config_per_platform(mock_def): diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 72f26ae33ad..a639b16893b 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -4,7 +4,6 @@ import logging import os from unittest.mock import Mock -from asynctest import patch import pytest from homeassistant import bootstrap @@ -12,6 +11,7 @@ import homeassistant.config as config_util from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/test_config.py b/tests/test_config.py index b92ea0e890b..ab9eeb639e6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,10 +4,7 @@ from collections import OrderedDict import copy import os from unittest import mock -from unittest.mock import Mock -import asynctest -from asynctest import CoroutineMock, patch import pytest import voluptuous as vol from voluptuous import Invalid, MultipleInvalid @@ -37,6 +34,7 @@ from homeassistant.loader import async_get_integration from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML +from tests.async_mock import AsyncMock, Mock, patch from tests.common import get_test_config_dir, patch_yaml_files CONFIG_DIR = get_test_config_dir() @@ -98,7 +96,7 @@ async def test_ensure_config_exists_creates_config(hass): If not creates a new config file. """ - with mock.patch("builtins.print") as mock_print: + with patch("builtins.print") as mock_print: await config_util.async_ensure_config_exists(hass) assert os.path.isfile(YAML_PATH) @@ -168,7 +166,7 @@ async def test_create_default_config_returns_none_if_write_error(hass): Non existing folder returns None. """ hass.config.config_dir = os.path.join(CONFIG_DIR, "non_existing_dir/") - with mock.patch("builtins.print") as mock_print: + with patch("builtins.print") as mock_print: assert await config_util.async_create_default_config(hass) is False assert mock_print.called @@ -180,6 +178,8 @@ def test_core_config_schema(): {"time_zone": "non-exist"}, {"latitude": "91"}, {"longitude": -181}, + {"external_url": "not an url"}, + {"internal_url": "not an url"}, {"customize": "bla"}, {"customize": {"light.sensor": 100}}, {"customize": {"entity_id": []}}, @@ -192,6 +192,8 @@ def test_core_config_schema(): "name": "Test name", "latitude": "-23.45", "longitude": "123.45", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, "customize": {"sensor.temperature": {"hidden": True}}, } @@ -245,15 +247,15 @@ async def test_entity_customization(hass): assert state.attributes["hidden"] -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.is_docker_env", return_value=False) +@patch("homeassistant.config.shutil") +@patch("homeassistant.config.os") +@patch("homeassistant.config.is_docker_env", return_value=False) def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass): """Test removal of library on upgrade from before 0.50.""" ha_version = "0.49.0" mock_os.path.isdir = mock.Mock(return_value=True) mock_open = mock.mock_open() - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -267,15 +269,15 @@ def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass): assert mock_shutil.rmtree.call_args == mock.call(hass_path) -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.is_docker_env", return_value=True) +@patch("homeassistant.config.shutil") +@patch("homeassistant.config.os") +@patch("homeassistant.config.is_docker_env", return_value=True) def test_remove_lib_on_upgrade_94(mock_docker, mock_os, mock_shutil, hass): """Test removal of library on upgrade from before 0.94 and in Docker.""" ha_version = "0.93.0.dev0" mock_os.path.isdir = mock.Mock(return_value=True) mock_open = mock.mock_open() - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -294,9 +296,9 @@ def test_process_config_upgrade(hass): ha_version = "0.92.0" mock_open = mock.mock_open() - with mock.patch( - "homeassistant.config.open", mock_open, create=True - ), mock.patch.object(config_util, "__version__", "0.91.0"): + with patch("homeassistant.config.open", mock_open, create=True), patch.object( + config_util, "__version__", "0.91.0" + ): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -312,7 +314,7 @@ def test_config_upgrade_same_version(hass): ha_version = __version__ mock_open = mock.mock_open() - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -326,7 +328,7 @@ def test_config_upgrade_no_file(hass): """Test update of version on upgrade, with no version file.""" mock_open = mock.mock_open() mock_open.side_effect = [FileNotFoundError(), mock.DEFAULT, mock.DEFAULT] - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member config_util.process_ha_config_upgrade(hass) @@ -344,6 +346,8 @@ async def test_loading_configuration_from_storage(hass, hass_storage): "longitude": 13, "time_zone": "Europe/Copenhagen", "unit_system": "metric", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", }, "key": "core.config", "version": 1, @@ -358,6 +362,8 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.external_url == "https://www.example.com" + assert hass.config.internal_url == "http://example.local" assert len(hass.config.whitelist_external_dirs) == 2 assert "/etc" in hass.config.whitelist_external_dirs assert hass.config.config_source == SOURCE_STORAGE @@ -373,6 +379,8 @@ async def test_updating_configuration(hass, hass_storage): "longitude": 13, "time_zone": "Europe/Copenhagen", "unit_system": "metric", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", }, "key": "core.config", "version": 1, @@ -430,6 +438,8 @@ async def test_loading_configuration(hass): CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, "time_zone": "America/New_York", "whitelist_external_dirs": "/etc", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", }, ) @@ -439,6 +449,8 @@ async def test_loading_configuration(hass): assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.external_url == "https://www.example.com" + assert hass.config.internal_url == "http://example.local" assert len(hass.config.whitelist_external_dirs) == 2 assert "/etc" in hass.config.whitelist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML @@ -455,6 +467,8 @@ async def test_loading_configuration_temperature_unit(hass): "name": "Huis", CONF_TEMPERATURE_UNIT: "C", "time_zone": "America/New_York", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", }, ) @@ -464,6 +478,8 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.external_url == "https://www.example.com" + assert hass.config.internal_url == "http://example.local" assert hass.config.config_source == config_util.SOURCE_YAML @@ -478,6 +494,8 @@ async def test_loading_configuration_from_packages(hass): "name": "Huis", CONF_TEMPERATURE_UNIT: "C", "time_zone": "Europe/Madrid", + "external_url": "https://www.example.com", + "internal_url": "http://example.local", "packages": { "package_1": {"wake_on_lan": None}, "package_2": { @@ -505,14 +523,14 @@ async def test_loading_configuration_from_packages(hass): ) -@asynctest.mock.patch("homeassistant.helpers.check_config.async_check_ha_config_file") +@patch("homeassistant.helpers.check_config.async_check_ha_config_file") async def test_check_ha_config_file_correct(mock_check, hass): """Check that restart propagates to stop.""" mock_check.return_value = check_config.HomeAssistantConfig() assert await config_util.async_check_ha_config_file(hass) is None -@asynctest.mock.patch("homeassistant.helpers.check_config.async_check_ha_config_file") +@patch("homeassistant.helpers.check_config.async_check_ha_config_file") async def test_check_ha_config_file_wrong(mock_check, hass): """Check that restart with a bad config doesn't propagate to stop.""" mock_check.return_value = check_config.HomeAssistantConfig() @@ -521,9 +539,7 @@ async def test_check_ha_config_file_wrong(mock_check, hass): assert await config_util.async_check_ha_config_file(hass) == "bad" -@asynctest.mock.patch( - "homeassistant.config.os.path.isfile", mock.Mock(return_value=True) -) +@patch("homeassistant.config.os.path.isfile", mock.Mock(return_value=True)) async def test_async_hass_config_yaml_merge(merge_log_err, hass): """Test merge during async config reload.""" config = { @@ -549,7 +565,7 @@ async def test_async_hass_config_yaml_merge(merge_log_err, hass): @pytest.fixture def merge_log_err(hass): """Patch _merge_log_error from packages.""" - with mock.patch("homeassistant.config._LOGGER.error") as logerr: + with patch("homeassistant.config._LOGGER.error") as logerr: yield logerr @@ -907,7 +923,7 @@ async def test_component_config_exceptions(hass, caplog): domain="test_domain", get_platform=Mock( return_value=Mock( - async_validate_config=CoroutineMock( + async_validate_config=AsyncMock( side_effect=ValueError("broken") ) ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 28746bbfbe0..592bc1d4656 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,9 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, patch -from asynctest import CoroutineMock import pytest from homeassistant import config_entries, data_entry_flow, loader @@ -12,6 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt +from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockEntity, @@ -55,8 +54,8 @@ async def test_call_setup_entry(hass): entry = MockConfigEntry(domain="comp") entry.add_to_hass(hass) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) - mock_migrate_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) + mock_migrate_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -81,8 +80,8 @@ async def test_call_async_migrate_entry(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro(True)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -107,8 +106,8 @@ async def test_call_async_migrate_entry_failure_false(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro(False)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(return_value=False) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -133,8 +132,8 @@ async def test_call_async_migrate_entry_failure_exception(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro(exception=Exception)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(side_effect=Exception) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -159,8 +158,8 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro()) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(return_value=None) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -185,7 +184,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): entry.version = 2 entry.add_to_hass(hass) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -212,7 +211,7 @@ async def test_remove_entry(hass, manager): assert result return result - mock_remove_entry = MagicMock(side_effect=lambda *args, **kwargs: mock_coro()) + mock_remove_entry = AsyncMock(return_value=None) entity = MockEntity(unique_id="1234", name="Test Entity") @@ -284,9 +283,9 @@ async def test_remove_entry(hass, manager): async def test_remove_entry_handles_callback_error(hass, manager): """Test that exceptions in the remove callback are handled.""" - mock_setup_entry = MagicMock(return_value=mock_coro(True)) - mock_unload_entry = MagicMock(return_value=mock_coro(True)) - mock_remove_entry = MagicMock(side_effect=lambda *args, **kwargs: mock_coro()) + mock_setup_entry = AsyncMock(return_value=True) + mock_unload_entry = AsyncMock(return_value=True) + mock_remove_entry = AsyncMock(return_value=None) mock_integration( hass, MockModule( @@ -344,7 +343,7 @@ async def test_remove_entry_raises(hass, manager): async def test_remove_entry_if_not_loaded(hass, manager): """Test that we can remove an entry that is not loaded.""" - mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=mock_unload_entry)) @@ -368,7 +367,7 @@ async def test_remove_entry_if_not_loaded(hass, manager): async def test_add_entry_calls_setup_entry(hass, manager): """Test we call setup_config_entry.""" - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -487,12 +486,12 @@ async def test_forward_entry_sets_up_component(hass): """Test we setup the component entry is forwarded to.""" entry = MockConfigEntry(domain="original") - mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_original_setup_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule("original", async_setup_entry=mock_original_setup_entry) ) - mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_forwarded_setup_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry) ) @@ -506,8 +505,8 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): """Test we do not set up entry if component setup fails.""" entry = MockConfigEntry(domain="original") - mock_setup = MagicMock(return_value=mock_coro(False)) - mock_setup_entry = MagicMock() + mock_setup = AsyncMock(return_value=False) + mock_setup_entry = AsyncMock() mock_integration( hass, MockModule( @@ -644,7 +643,7 @@ async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain="test") - mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.test", None) @@ -660,7 +659,7 @@ async def test_setup_raise_not_ready(hass, caplog): assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY mock_setup_entry.side_effect = None - mock_setup_entry.return_value = mock_coro(True) + mock_setup_entry.return_value = True await p_setup(None) assert entry.state == config_entries.ENTRY_STATE_LOADED @@ -670,7 +669,7 @@ async def test_setup_retrying_during_unload(hass): """Test if we unload an entry that is in retry mode.""" entry = MockConfigEntry(domain="test") - mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.test", None) @@ -722,8 +721,8 @@ async def test_entry_setup_succeed(hass, manager): entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_NOT_LOADED) entry.add_to_hass(hass) - mock_setup = MagicMock(return_value=mock_coro(True)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -752,8 +751,8 @@ async def test_entry_setup_invalid_state(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - mock_setup = MagicMock(return_value=mock_coro(True)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -773,7 +772,7 @@ async def test_entry_unload_succeed(hass, manager): entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) entry.add_to_hass(hass) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) @@ -795,7 +794,7 @@ async def test_entry_unload_failed_to_load(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) @@ -816,7 +815,7 @@ async def test_entry_unload_invalid_state(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) @@ -832,9 +831,9 @@ async def test_entry_reload_succeed(hass, manager): entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) entry.add_to_hass(hass) - async_setup = MagicMock(return_value=mock_coro(True)) - async_setup_entry = MagicMock(return_value=mock_coro(True)) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -867,9 +866,9 @@ async def test_entry_reload_not_loaded(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_setup = MagicMock(return_value=mock_coro(True)) - async_setup_entry = MagicMock(return_value=mock_coro(True)) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -901,9 +900,9 @@ async def test_entry_reload_error(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_setup = MagicMock(return_value=mock_coro(True)) - async_setup_entry = MagicMock(return_value=mock_coro(True)) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -935,8 +934,7 @@ async def test_init_custom_integration(hass): ) with pytest.raises(data_entry_flow.UnknownHandler): with patch( - "homeassistant.loader.async_get_integration", - return_value=mock_coro(integration), + "homeassistant.loader.async_get_integration", return_value=integration, ): await hass.config_entries.flow.async_init("bla") @@ -970,8 +968,8 @@ async def test_reload_entry_entity_registry_works(hass): domain="comp", state=config_entries.ENTRY_STATE_LOADED ) config_entry.add_to_hass(hass) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) - mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) + mock_unload_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule( @@ -1018,7 +1016,7 @@ async def test_reload_entry_entity_registry_works(hass): async def test_unqiue_id_persisted(hass, manager): """Test that a unique ID is stored in the config entry.""" - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1054,9 +1052,9 @@ async def test_unique_id_existing_entry(hass, manager): unique_id="mock-unique-id", ).add_to_hass(hass) - async_setup_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) - async_unload_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) - async_remove_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + async_remove_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -1207,8 +1205,7 @@ async def test_unique_id_in_progress(hass, manager): async def test_finish_flow_aborts_progress(hass, manager): """Test that when finishing a flow, we abort other flows in progress with unique ID.""" mock_integration( - hass, - MockModule("comp", async_setup_entry=MagicMock(return_value=mock_coro(True))), + hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)), ) mock_entity_platform(hass, "config_flow.comp", None) @@ -1245,7 +1242,7 @@ async def test_finish_flow_aborts_progress(hass, manager): async def test_unique_id_ignore(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID.""" - async_setup_entry = MagicMock(return_value=mock_coro(False)) + async_setup_entry = AsyncMock(return_value=False) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1287,7 +1284,7 @@ async def test_unique_id_ignore(hass, manager): async def test_unignore_step_form(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1331,7 +1328,7 @@ async def test_unignore_step_form(hass, manager): async def test_unignore_create_entry(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1378,7 +1375,7 @@ async def test_unignore_create_entry(hass, manager): async def test_unignore_default_impl(hass, manager): """Test that resdicovery is a no-op by default.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1409,7 +1406,7 @@ async def test_unignore_default_impl(hass, manager): async def test_partial_flows_hidden(hass, manager): """Test that flows that don't have a cur_step and haven't finished initing are hidden.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) await async_setup_component(hass, "persistent_notification", {}) @@ -1478,7 +1475,7 @@ async def test_async_setup_init_entry(hass): ) return True - async_setup_entry = CoroutineMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule( diff --git a/tests/test_core.py b/tests/test_core.py index 8b4a2ae9f84..3bc001b78b6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,7 +7,6 @@ import logging import os from tempfile import TemporaryDirectory import unittest -from unittest.mock import MagicMock, patch import pytest import pytz @@ -22,6 +21,7 @@ from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, @@ -35,6 +35,7 @@ from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.async_mock import MagicMock, Mock, patch from tests.common import async_mock_service, get_test_home_assistant PST = pytz.timezone("America/Los_Angeles") @@ -913,6 +914,8 @@ class TestConfig(unittest.TestCase): "version": __version__, "config_source": "default", "safe_mode": False, + "external_url": None, + "internal_url": None, } assert expected == self.config.as_dict() @@ -948,7 +951,7 @@ class TestConfig(unittest.TestCase): self.config.is_allowed_path(None) -async def test_event_on_update(hass, hass_storage): +async def test_event_on_update(hass): """Test that event is fired on update.""" events = [] @@ -1281,3 +1284,49 @@ def test_valid_entity_id(): "light.something_yoo", ]: assert ha.valid_entity_id(valid), valid + + +async def test_migration_base_url(hass, hass_storage): + """Test that we migrate base url to internal/external url.""" + config = ha.Config(hass) + stored = {"version": 1, "data": {}} + hass_storage[ha.CORE_STORAGE_KEY] = stored + with patch.object(hass.bus, "async_listen_once") as mock_listen: + # Empty config + await config.async_load() + assert len(mock_listen.mock_calls) == 0 + + # With just a name + stored["data"] = {"location_name": "Test Name"} + await config.async_load() + assert len(mock_listen.mock_calls) == 1 + + # With external url + stored["data"]["external_url"] = "https://example.com" + await config.async_load() + assert len(mock_listen.mock_calls) == 1 + + # Test that the event listener works + assert mock_listen.mock_calls[0][1][0] == EVENT_HOMEASSISTANT_START + + # External + hass.config.api = Mock(deprecated_base_url="https://loaded-example.com") + await mock_listen.mock_calls[0][1][1](None) + assert config.external_url == "https://loaded-example.com" + + # Internal + for internal in ("http://hass.local", "http://192.168.1.100:8123"): + hass.config.api = Mock(deprecated_base_url=internal) + await mock_listen.mock_calls[0][1][1](None) + assert config.internal_url == internal + + +async def test_additional_data_in_core_config(hass, hass_storage): + """Test that we can handle additional data in core configuration.""" + config = ha.Config(hass) + hass_storage[ha.CORE_STORAGE_KEY] = { + "version": 1, + "data": {"location_name": "Test Name", "additional_valid_key": "value"}, + } + await config.async_load() + assert config.location_name == "Test Name" diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 664304c9ef6..64b8587fe7c 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -26,7 +26,6 @@ def manager(): flow = handler() flow.init_step = context.get("init_step", "init") - flow.source = context.get("source") return flow async def async_finish_flow(self, flow, result): @@ -59,7 +58,14 @@ async def test_configure_reuses_handler_instance(manager): assert form["errors"]["base"] == "1" form = await manager.async_configure(form["flow_id"]) assert form["errors"]["base"] == "2" - assert len(manager.async_progress()) == 1 + assert manager.async_progress() == [ + { + "flow_id": form["flow_id"], + "handler": "test", + "step_id": "init", + "context": {}, + } + ] assert len(manager.mock_created_entries) == 0 diff --git a/tests/test_loader.py b/tests/test_loader.py index 745bb9c8c2c..eb99cb3a8ea 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,11 +1,11 @@ """Test to verify that we can load components.""" -from asynctest.mock import ANY, patch import pytest from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light import homeassistant.loader as loader +from tests.async_mock import ANY, patch from tests.common import MockModule, async_mock_service, mock_integration diff --git a/tests/test_main.py b/tests/test_main.py index 5ec6460301f..40c34b77b50 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,9 @@ """Test methods in __main__.""" -from unittest.mock import PropertyMock, patch - from homeassistant import __main__ as main from homeassistant.const import REQUIRED_PYTHON_VER +from tests.async_mock import PropertyMock, patch + @patch("sys.exit") def test_validate_python(mock_exit): diff --git a/tests/test_requirements.py b/tests/test_requirements.py index e95db4e533d..f98485e8006 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,7 +1,6 @@ """Test requirements module.""" import os from pathlib import Path -from unittest.mock import call, patch import pytest @@ -15,12 +14,8 @@ from homeassistant.requirements import ( async_process_requirements, ) -from tests.common import ( - MockModule, - get_test_home_assistant, - mock_coro, - mock_integration, -) +from tests.async_mock import call, patch +from tests.common import MockModule, mock_integration def env_without_wheel_links(): @@ -30,58 +25,42 @@ def env_without_wheel_links(): return env -class TestRequirements: - """Test the requirements module.""" - - hass = None - backup_cache = None - - # pylint: disable=invalid-name, no-self-use - def setup_method(self, method): - """Set up the test.""" - self.hass = get_test_home_assistant() - - def teardown_method(self, method): - """Clean up.""" - self.hass.stop() - - @patch("os.path.dirname") - @patch("homeassistant.util.package.is_virtual_env", return_value=True) - @patch("homeassistant.util.package.is_docker_env", return_value=False) - @patch("homeassistant.util.package.install_package", return_value=True) - @patch.dict(os.environ, env_without_wheel_links(), clear=True) - def test_requirement_installed_in_venv( - self, mock_install, mock_denv, mock_venv, mock_dirname +async def test_requirement_installed_in_venv(hass): + """Test requirement installed in virtual environment.""" + with patch("os.path.dirname", return_value="ha_package_path"), patch( + "homeassistant.util.package.is_virtual_env", return_value=True + ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_install, patch.dict( + os.environ, env_without_wheel_links(), clear=True ): - """Test requirement installed in virtual environment.""" - mock_dirname.return_value = "ha_package_path" - self.hass.config.skip_pip = False - mock_integration(self.hass, MockModule("comp", requirements=["package==0.0.1"])) - assert setup.setup_component(self.hass, "comp", {}) - assert "comp" in self.hass.config.components + hass.config.skip_pip = False + mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) + assert await setup.async_setup_component(hass, "comp", {}) + assert "comp" in hass.config.components assert mock_install.call_args == call( "package==0.0.1", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), no_cache_dir=False, ) - @patch("os.path.dirname") - @patch("homeassistant.util.package.is_virtual_env", return_value=False) - @patch("homeassistant.util.package.is_docker_env", return_value=False) - @patch("homeassistant.util.package.install_package", return_value=True) - @patch.dict(os.environ, env_without_wheel_links(), clear=True) - def test_requirement_installed_in_deps( - self, mock_install, mock_denv, mock_venv, mock_dirname + +async def test_requirement_installed_in_deps(hass): + """Test requirement installed in deps directory.""" + with patch("os.path.dirname", return_value="ha_package_path"), patch( + "homeassistant.util.package.is_virtual_env", return_value=False + ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_install, patch.dict( + os.environ, env_without_wheel_links(), clear=True ): - """Test requirement installed in deps directory.""" - mock_dirname.return_value = "ha_package_path" - self.hass.config.skip_pip = False - mock_integration(self.hass, MockModule("comp", requirements=["package==0.0.1"])) - assert setup.setup_component(self.hass, "comp", {}) - assert "comp" in self.hass.config.components + hass.config.skip_pip = False + mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) + assert await setup.async_setup_component(hass, "comp", {}) + assert "comp" in hass.config.components assert mock_install.call_args == call( "package==0.0.1", - target=self.hass.config.path("deps"), + target=hass.config.path("deps"), constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), no_cache_dir=False, ) @@ -239,7 +218,6 @@ async def test_discovery_requirements_ssdp(hass): ) with patch( "homeassistant.requirements.async_process_requirements", - side_effect=lambda _, _2, _3: mock_coro(), ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") @@ -262,7 +240,6 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): with patch( "homeassistant.requirements.async_process_requirements", - side_effect=lambda _, _2, _3: mock_coro(), ) as mock_process: await async_get_integration_with_requirements(hass, "comp") diff --git a/tests/test_setup.py b/tests/test_setup.py index daa9c6e8406..4197fe7370a 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,13 +1,14 @@ """Test component/platform setup.""" # pylint: disable=protected-access +import asyncio import logging import os import threading -from unittest import mock +import pytest import voluptuous as vol -from homeassistant import setup +from homeassistant import config_entries, setup import homeassistant.config as config_util from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import callback @@ -18,7 +19,9 @@ from homeassistant.helpers.config_validation import ( ) import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( + MockConfigEntry, MockModule, MockPlatform, assert_setup_component, @@ -34,6 +37,19 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) _LOGGER = logging.getLogger(__name__) +@pytest.fixture(autouse=True) +def mock_handlers(): + """Mock config flows.""" + + class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + with patch.dict(config_entries.HANDLERS, {"comp": MockFlowHandler}): + yield + + class TestSetup: """Test the bootstrap utils.""" @@ -239,7 +255,7 @@ class TestSetup: def test_component_not_double_initialized(self): """Test we do not set up a component twice.""" - mock_setup = mock.MagicMock(return_value=True) + mock_setup = Mock(return_value=True) mock_integration(self.hass, MockModule("comp", setup=mock_setup)) @@ -251,7 +267,7 @@ class TestSetup: assert setup.setup_component(self.hass, "comp", {}) assert not mock_setup.called - @mock.patch("homeassistant.util.package.install_package", return_value=False) + @patch("homeassistant.util.package.install_package", return_value=False) def test_component_not_installed_if_requirement_fails(self, mock_install): """Component setup should fail if requirement can't install.""" self.hass.config.skip_pip = False @@ -350,7 +366,7 @@ class TestSetup: {"valid": True}, extra=vol.PREVENT_EXTRA ) - mock_setup = mock.MagicMock(spec_set=True) + mock_setup = Mock(spec_set=True) mock_entity_platform( self.hass, @@ -469,7 +485,7 @@ async def test_component_cannot_depend_config(hass): async def test_component_warn_slow_setup(hass): """Warn we log when a component setup takes a long time.""" mock_integration(hass, MockModule("test_component1")) - with mock.patch.object(hass.loop, "call_later", mock.MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: result = await setup.async_setup_component(hass, "test_component1", {}) assert result assert mock_call.called @@ -488,7 +504,7 @@ async def test_platform_no_warn_slow(hass): mock_integration( hass, MockModule("test_component1", platform_schema=PLATFORM_SCHEMA) ) - with mock.patch.object(hass.loop, "call_later", mock.MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: result = await setup.async_setup_component(hass, "test_component1", {}) assert result assert not mock_call.called @@ -524,7 +540,30 @@ async def test_when_setup_already_loaded(hass): async def test_setup_import_blows_up(hass): """Test that we handle it correctly when importing integration blows up.""" - with mock.patch( + with patch( "homeassistant.loader.Integration.get_component", side_effect=ValueError ): assert not await setup.async_setup_component(hass, "sun", {}) + + +async def test_parallel_entry_setup(hass): + """Test config entries are set up in parallel.""" + MockConfigEntry(domain="comp", data={"value": 1}).add_to_hass(hass) + MockConfigEntry(domain="comp", data={"value": 2}).add_to_hass(hass) + + calls = [] + + async def mock_async_setup_entry(hass, entry): + """Mock setting up an entry.""" + calls.append(entry.data["value"]) + await asyncio.sleep(0) + calls.append(entry.data["value"]) + return True + + mock_integration( + hass, MockModule("comp", async_setup_entry=mock_async_setup_entry,), + ) + mock_entity_platform(hass, "config_flow.comp", None) + await setup.async_setup_component(hass, "comp", {}) + + assert calls == [1, 2, 1, 2] diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 065ea3e3980..6535f5aa1f5 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -3,7 +3,7 @@ Provide a mock alarm_control_panel platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -32,12 +32,12 @@ def init(empty=False): if empty else { "arm_code": MockAlarm( - name=f"Alarm arm code", + name="Alarm arm code", code_arm_required=True, unique_id="unique_arm_code", ), "no_arm_code": MockAlarm( - name=f"Alarm no arm code", + name="Alarm no arm code", code_arm_required=False, unique_id="unique_no_arm_code", ), @@ -52,7 +52,7 @@ async def async_setup_platform( async_add_entities_callback(list(ENTITIES.values())) -class MockAlarm(MockEntity, AlarmControlPanel): +class MockAlarm(MockEntity, AlarmControlPanelEntity): """Mock Alarm control panel class.""" def __init__(self, **values): diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py index bcff0adb4e4..13a877f7e8c 100644 --- a/tests/testing_config/custom_components/test/binary_sensor.py +++ b/tests/testing_config/custom_components/test/binary_sensor.py @@ -3,7 +3,7 @@ Provide a mock binary sensor platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity from tests.common import MockEntity @@ -36,7 +36,7 @@ async def async_setup_platform( async_add_entities_callback(list(ENTITIES.values())) -class MockBinarySensor(MockEntity, BinarySensorDevice): +class MockBinarySensor(MockEntity, BinarySensorEntity): """Mock Binary Sensor class.""" @property diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index bdaacfa4e3c..095489ce7b4 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, SUPPORT_STOP_TILT, - CoverDevice, + CoverEntity, ) from tests.common import MockEntity @@ -29,29 +29,29 @@ def init(empty=False): if empty else [ MockCover( - name=f"Simple cover", + name="Simple cover", is_on=True, - unique_id=f"unique_cover", + unique_id="unique_cover", supports_tilt=False, ), MockCover( - name=f"Set position cover", + name="Set position cover", is_on=True, - unique_id=f"unique_set_pos_cover", + unique_id="unique_set_pos_cover", current_cover_position=50, supports_tilt=False, ), MockCover( - name=f"Set tilt position cover", + name="Set tilt position cover", is_on=True, - unique_id=f"unique_set_pos_tilt_cover", + unique_id="unique_set_pos_tilt_cover", current_cover_tilt_position=50, supports_tilt=True, ), MockCover( - name=f"Tilt cover", + name="Tilt cover", is_on=True, - unique_id=f"unique_tilt_cover", + unique_id="unique_tilt_cover", supports_tilt=True, ), ] @@ -65,7 +65,7 @@ async def async_setup_platform( async_add_entities_callback(ENTITIES) -class MockCover(MockEntity, CoverDevice): +class MockCover(MockEntity, CoverEntity): """Mock Cover class.""" @property diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index d3f96c367d8..863412fe747 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -3,7 +3,7 @@ Provide a mock light platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.light import Light +from homeassistant.components.light import LightEntity from homeassistant.const import STATE_OFF, STATE_ON from tests.common import MockToggleEntity @@ -33,7 +33,7 @@ async def async_setup_platform( async_add_entities_callback(ENTITIES) -class MockLight(MockToggleEntity, Light): +class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" brightness = None diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index 24b04903541..a6ce9e102d5 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -3,7 +3,7 @@ Provide a mock lock platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.lock import SUPPORT_OPEN, LockDevice +from homeassistant.components.lock import SUPPORT_OPEN, LockEntity from tests.common import MockEntity @@ -19,13 +19,13 @@ def init(empty=False): if empty else { "support_open": MockLock( - name=f"Support open Lock", + name="Support open Lock", is_locked=True, supported_features=SUPPORT_OPEN, unique_id="unique_support_open", ), "no_support_open": MockLock( - name=f"No support open Lock", + name="No support open Lock", is_locked=True, supported_features=0, unique_id="unique_no_support_open", @@ -41,7 +41,7 @@ async def async_setup_platform( async_add_entities_callback(list(ENTITIES.values())) -class MockLock(MockEntity, LockDevice): +class MockLock(MockEntity, LockEntity): """Mock Lock class.""" @property diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 33280895ba2..b60b4097c52 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,12 +1,13 @@ """Tests for async util methods from Python source.""" import asyncio from unittest import TestCase -from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.util import async_ as hasync +from tests.async_mock import MagicMock, Mock, patch + @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 2ffca07082b..ef5ecd898d7 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,12 +1,13 @@ """Test Home Assistant util methods.""" from datetime import datetime, timedelta -from unittest.mock import MagicMock, patch import pytest from homeassistant import util import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, patch + def test_sanitize_filename(): """Test sanitize_filename.""" diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 258f266ff78..c2b6a428515 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -7,7 +7,6 @@ import os import sys from tempfile import mkdtemp import unittest -from unittest.mock import Mock import pytest @@ -20,6 +19,8 @@ from homeassistant.util.json import ( save_json, ) +from tests.async_mock import Mock + # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} TEST_JSON_B = {"a": "one", "B": 2} diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 3f03619a052..403e24121ad 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,12 +1,11 @@ """Test Home Assistant location util methods.""" -from unittest.mock import Mock, patch - import aiohttp import pytest import homeassistant.util.location as location_util -from tests.common import load_fixture, mock_coro +from tests.async_mock import Mock, patch +from tests.common import load_fixture # Paris COORDINATES_PARIS = (48.864716, 2.349014) @@ -109,7 +108,7 @@ async def test_detect_location_info_ip_api(aioclient_mock, session): """Test detect location info using ip-api.com.""" aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - with patch("homeassistant.util.location._get_ipapi", return_value=mock_coro(None)): + with patch("homeassistant.util.location._get_ipapi", return_value=None): info = await location_util.async_detect_location_info(session, _test_real=True) assert info is not None @@ -128,9 +127,9 @@ async def test_detect_location_info_ip_api(aioclient_mock, session): async def test_detect_location_info_both_queries_fail(session): """Ensure we return None if both queries fail.""" - with patch( - "homeassistant.util.location._get_ipapi", return_value=mock_coro(None) - ), patch("homeassistant.util.location._get_ip_api", return_value=mock_coro(None)): + with patch("homeassistant.util.location._get_ipapi", return_value=None), patch( + "homeassistant.util.location._get_ip_api", return_value=None + ): info = await location_util.async_detect_location_info(session, _test_real=True) assert info is None diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index c2c9d4803f9..2d05157e26f 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -3,6 +3,8 @@ import asyncio import logging import threading +import pytest + import homeassistant.util.logging as logging_util @@ -65,6 +67,7 @@ async def test_async_handler_thread_log(loop): assert queue.empty() +@pytest.mark.no_fail_on_log_exception async def test_async_create_catching_coro(hass, caplog): """Test exception logging of wrapped coroutine.""" diff --git a/tests/util/test_network.py b/tests/util/test_network.py index c4c33c8d187..2cd710e1d6c 100644 --- a/tests/util/test_network.py +++ b/tests/util/test_network.py @@ -38,3 +38,34 @@ def test_is_local(): assert network_util.is_local(ip_address("192.168.0.1")) assert network_util.is_local(ip_address("127.0.0.1")) assert not network_util.is_local(ip_address("208.5.4.2")) + + +def test_is_ip_address(): + """Test if strings are IP addresses.""" + assert network_util.is_ip_address("192.168.0.1") + assert network_util.is_ip_address("8.8.8.8") + assert network_util.is_ip_address("::ffff:127.0.0.0") + assert not network_util.is_ip_address("192.168.0.999") + assert not network_util.is_ip_address("192.168.0.0/24") + assert not network_util.is_ip_address("example.com") + + +def test_normalize_url(): + """Test the normalizing of URLs.""" + assert network_util.normalize_url("http://example.com") == "http://example.com" + assert network_util.normalize_url("https://example.com") == "https://example.com" + assert network_util.normalize_url("https://example.com/") == "https://example.com" + assert ( + network_util.normalize_url("https://example.com:443") == "https://example.com" + ) + assert network_util.normalize_url("http://example.com:80") == "http://example.com" + assert ( + network_util.normalize_url("https://example.com:80") == "https://example.com:80" + ) + assert ( + network_util.normalize_url("http://example.com:443") == "http://example.com:443" + ) + assert ( + network_util.normalize_url("https://example.com:443/test/") + == "https://example.com/test" + ) diff --git a/tests/util/test_package.py b/tests/util/test_package.py index ca4ed83734a..c9f5183249e 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -4,13 +4,14 @@ import logging import os from subprocess import PIPE import sys -from unittest.mock import MagicMock, call, patch import pkg_resources import pytest import homeassistant.util.package as package +from tests.async_mock import MagicMock, call, patch + RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") ) @@ -70,13 +71,11 @@ def mock_venv(): yield mock -@asyncio.coroutine def mock_async_subprocess(): """Return an async Popen mock.""" async_popen = MagicMock() - @asyncio.coroutine - def communicate(input=None): + async def communicate(input=None): """Communicate mock.""" stdout = bytes("/deps_dir/lib_dir", "utf-8") return (stdout, None) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 140859ccb73..4d6f4ce3ac9 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -3,7 +3,6 @@ import io import logging import os import unittest -from unittest.mock import patch import pytest @@ -12,6 +11,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.util.yaml as yaml from homeassistant.util.yaml import loader as yaml_loader +from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files